Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,11 @@ internal class FingerprintCaptureViewModel @Inject constructor(
liveFeedbackState = LiveFeedbackState.START
stopLiveFeedbackTask?.cancel()
liveFeedbackTask = viewModelScope.launch {
scannerManager.scanner.startLiveFeedback()
try {
scannerManager.scanner.startLiveFeedback()
} catch (e: Throwable) {
handleScannerCommunicationsError(e)
}
}
}
}
Expand Down Expand Up @@ -354,7 +358,7 @@ internal class FingerprintCaptureViewModel @Inject constructor(
)

handleCaptureSuccess(capturedFingerprint)
} catch (ex: CancellationException) {
} catch (_: CancellationException) {
// ignore cancellation exception, but log behaviour
Simber.d("Fingerprint scanning was cancelled")
} catch (ex: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.simprints.fingerprint.capture.screen

import android.content.Context
import androidx.preference.PreferenceManager
import com.simprints.fingerprint.infra.scanner.ScannerManager
import com.simprints.fingerprint.infra.scanner.capture.FingerprintScanState
import com.simprints.fingerprint.infra.scanner.capture.FingerprintScanningStatusTracker
Expand All @@ -9,14 +10,13 @@ import com.simprints.infra.config.store.models.Vero2Configuration
import com.simprints.infra.config.store.models.Vero2Configuration.LedsMode.BASIC
import com.simprints.infra.config.store.models.Vero2Configuration.LedsMode.VISUAL_SCAN_FEEDBACK
import com.simprints.infra.config.sync.ConfigManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext

@Singleton
class ObserveFingerprintScanStatusUseCase @Inject constructor(
Expand All @@ -27,7 +27,6 @@ class ObserveFingerprintScanStatusUseCase @Inject constructor(
@ApplicationContext private val context: Context,
) {
private var observeJob: Job? = null
private var previousState: FingerprintScanState = FingerprintScanState.Idle
private var ledsMode: Vero2Configuration.LedsMode? = BASIC
private val preference = PreferenceManager.getDefaultSharedPreferences(context)

Expand All @@ -39,22 +38,23 @@ class ObserveFingerprintScanStatusUseCase @Inject constructor(
ledsMode = configManager.getProjectConfiguration().fingerprint?.getSdkConfiguration(
fingerprintSdk
)?.vero2?.ledsMode
statusTracker.state.collect { state ->
provideFeedback(state)
previousState = state
launch {
statusTracker.scanCompleted.collect {
playRemoveFingerAudio()
}
}
statusTracker.state.collect {
provideFeedback(it)
}
}
}


private suspend fun provideFeedback(state: FingerprintScanState) {
when (state) {
is FingerprintScanState.Idle -> turnOnFlashingWhiteSmileLeds()
is FingerprintScanState.Scanning -> turnOffSmileLeds()
is FingerprintScanState.ScanCompleted -> playRemoveFingerAudio()
is FingerprintScanState.ImageQualityChecking.Good -> setUiAfterScan(true)
is FingerprintScanState.ImageQualityChecking.Bad -> setUiAfterScan(false)
}
private suspend fun provideFeedback(state: FingerprintScanState) = when (state) {
is FingerprintScanState.Idle -> turnOnFlashingWhiteSmileLeds()
is FingerprintScanState.Scanning -> turnOffSmileLeds()
is FingerprintScanState.ScanCompleted -> playRemoveFingerAudio()
is FingerprintScanState.ImageQualityChecking.Good -> setUiAfterScan(true)
is FingerprintScanState.ImageQualityChecking.Bad -> setUiAfterScan(false)
}

fun stopObserving() {
Expand All @@ -76,23 +76,19 @@ class ObserveFingerprintScanStatusUseCase @Inject constructor(
}

private suspend fun setUiAfterScan(isGoodScan: Boolean) {
// Check if the previous state was ScanCompleted to avoid displaying the bad or good scan UI twice
// There's no need to check the configuration, as the good/bad scan visual notifications apply across all LED modes.
if (previousState == FingerprintScanState.ScanCompleted) {
with(scannerManager.scanner) {
if (isGoodScan) setUiGoodCapture()
else setUiBadCapture()
with(scannerManager.scanner) {
if (isGoodScan) setUiGoodCapture()
else setUiBadCapture()

//Wait before turn of the leds
delay(LONG_DELAY)
turnOffSmileLeds()
}
//Wait before turn of the leds
delay(LONG_DELAY)
turnOffSmileLeds()
}

}

private fun playRemoveFingerAudio() {
// Verify that the previous state was not "ScanCompleted" to prevent the sound from playing twice.
if (previousState == FingerprintScanState.ScanCompleted) return

if (isAudioEnabled()) playAudioBeep()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.simprints.fingerprint.capture.screen
import android.content.Context
import android.media.MediaPlayer
import com.simprints.fingerprint.capture.R
import com.simprints.infra.logging.Simber
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

Expand All @@ -14,6 +15,7 @@ class PlayAudioBeepUseCase @Inject constructor(
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer.create(context, R.raw.beep)
}
Simber.d("Playing beep sound")
mediaPlayer?.start()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.simprints.fingerprint.infra.scanner.capture

import com.simprints.core.DispatcherIO
import com.simprints.fingerprint.infra.scanner.exceptions.unexpected.NullScannerException
import com.simprints.fingerprint.infra.scanner.v2.tools.ScannerUiHelper
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -12,7 +11,6 @@ import com.simprints.fingerprint.infra.scanner.v2.scanner.Scanner as ScannerV2
@Singleton
class FingerprintCaptureWrapperFactory @Inject constructor(
@DispatcherIO private val ioDispatcher: CoroutineDispatcher,
private val scannerUiHelper: ScannerUiHelper,
private val scanningStatusTracker: FingerprintScanningStatusTracker
) {
private var _captureWrapper: FingerprintCaptureWrapper? = null
Expand All @@ -21,13 +19,10 @@ class FingerprintCaptureWrapperFactory @Inject constructor(
get() = _captureWrapper ?: throw NullScannerException()

fun createV1(scannerV1: ScannerV1) {
_captureWrapper =
FingerprintCaptureWrapperV1(scannerV1, ioDispatcher)
_captureWrapper = FingerprintCaptureWrapperV1(scannerV1, ioDispatcher)
}

fun createV2(scannerV2: ScannerV2) {
_captureWrapper = FingerprintCaptureWrapperV2(
scannerV2, scannerUiHelper, ioDispatcher, scanningStatusTracker
)
_captureWrapper = FingerprintCaptureWrapperV2(scannerV2, scanningStatusTracker)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,159 +12,99 @@ import com.simprints.fingerprint.infra.scanner.v2.domain.main.message.un20.model
import com.simprints.fingerprint.infra.scanner.v2.domain.main.message.un20.models.Dpi
import com.simprints.fingerprint.infra.scanner.v2.domain.main.message.un20.models.ImageFormatData
import com.simprints.fingerprint.infra.scanner.v2.scanner.Scanner
import com.simprints.fingerprint.infra.scanner.v2.tools.ScannerUiHelper
import com.simprints.fingerprint.infra.scanner.v2.tools.wrapErrorFromScanner
import io.reactivex.Completable
import io.reactivex.Single
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.rx2.await
import kotlinx.coroutines.withContext
import com.simprints.fingerprint.infra.scanner.v2.tools.runWithErrorWrapping

internal class FingerprintCaptureWrapperV2(
private val scannerV2: Scanner,
private val scannerUiHelper: ScannerUiHelper,
private val ioDispatcher: CoroutineDispatcher,
private val tracker: FingerprintScanningStatusTracker,
) : FingerprintCaptureWrapper {

override suspend fun acquireImageDistortionMatrixConfiguration(): ByteArray =
withContext(ioDispatcher) {
scannerV2.acquireImageDistortionConfigurationMatrix()
.switchIfEmpty(Single.error(NoImageDistortionConfigurationMatrixException()))
.wrapErrorsFromScanner().await()
}
override suspend fun acquireImageDistortionMatrixConfiguration(): ByteArray = runWithErrorWrapping {
scannerV2.acquireImageDistortionConfigurationMatrix()
?: throw NoImageDistortionConfigurationMatrixException("Failed to acquire image distortion configuration matrix")
}

override suspend fun acquireFingerprintImage(): AcquireFingerprintImageResponse =
withContext(ioDispatcher) {
scannerV2.acquireImage(IMAGE_FORMAT).map { imageBytes ->
AcquireFingerprintImageResponse(imageBytes.image)
}
.switchIfEmpty(Single.error(NoFingerDetectedException("Failed to acquire image")))
.wrapErrorsFromScanner()
.await()
runWithErrorWrapping {
scannerV2.acquireImage(IMAGE_FORMAT)?.image?.let { imageBytes ->
AcquireFingerprintImageResponse(imageBytes)
} ?: throw NoFingerDetectedException("Failed to acquire image")
}


override suspend fun acquireUnprocessedImage(
captureDpi: Dpi?,
): AcquireUnprocessedImageResponse =

withContext(ioDispatcher) {
require(captureDpi != null && (captureDpi.value in MIN_CAPTURE_DPI..MAX_CAPTURE_DPI)) {
"Capture DPI must be between $MIN_CAPTURE_DPI and $MAX_CAPTURE_DPI"
}
// Capture fingerprint and ensure it's OK
scannerV2.captureFingerprint()
.ensureCaptureResultOkOrError()
.andThen(Completable.fromAction {
tracker.completeScan()
}).andThen(acquireUnprocessedImage())
.switchIfEmpty(Single.error(NoFingerDetectedException("Failed to acquire unprocessed image data")))
.wrapErrorsFromScanner().await()

): AcquireUnprocessedImageResponse = runWithErrorWrapping {
require(captureDpi != null && (captureDpi.value in MIN_CAPTURE_DPI..MAX_CAPTURE_DPI)) {
"Capture DPI must be between $MIN_CAPTURE_DPI and $MAX_CAPTURE_DPI"
}


private fun acquireUnprocessedImage() =
scannerV2.acquireUnprocessedImage(IMAGE_FORMAT)
.map { imageData ->
AcquireUnprocessedImageResponse(
RawUnprocessedImage(
imageData.image
)
// Capture fingerprint and ensure it's OK
scannerV2.captureFingerprint().ensureCaptureResultOkOrError()
tracker.completeScan()
// Transfer the unprocessed image from the scanner
scannerV2.acquireUnprocessedImage(IMAGE_FORMAT)?.image?.let { imageData ->
AcquireUnprocessedImageResponse(
RawUnprocessedImage(
imageData
)
}
)
} ?: throw NoFingerDetectedException("Failed to acquire unprocessed image data")
}

override suspend fun acquireFingerprintTemplate(
captureDpi: Dpi?,
timeOutMs: Int,
qualityThreshold: Int,
allowLowQualityExtraction: Boolean
): AcquireFingerprintTemplateResponse = withContext(ioDispatcher) {
): AcquireFingerprintTemplateResponse = runWithErrorWrapping {
require(captureDpi != null && (captureDpi.value in MIN_CAPTURE_DPI..MAX_CAPTURE_DPI)) {
"Capture DPI must be between $MIN_CAPTURE_DPI and $MAX_CAPTURE_DPI"
}
scannerV2
.captureFingerprint(captureDpi)
.ensureCaptureResultOkOrError()
.andThen(Completable.fromAction {
tracker.completeScan()
})
.andThen(scannerV2.getImageQualityScore())
.switchIfEmpty(Single.error(NoFingerDetectedException("Failed to acquire image quality score")))
.validateMinimumFingerImageQuality()
.flatMap { qualityScore ->
// If the quality score is below the threshold and we don't allow low quality extraction, return an empty template
if (qualityScore < qualityThreshold && !allowLowQualityExtraction) {
Single.just(AcquireFingerprintTemplateResponse(ByteArray(0), templateFormat, qualityScore))
} else {
Single.just(qualityScore)
.acquireTemplateAndAssembleResponse()
.switchIfEmpty(Single.error(NoFingerDetectedException("Failed to acquire template")))
.ifNoFingerDetectedThenSetBadScanLedState()
.wrapErrorsFromScanner()
}
}.wrapErrorsFromScanner().await()
scannerV2.captureFingerprint(captureDpi).ensureCaptureResultOkOrError()
tracker.completeScan()
val qualityScore = scannerV2.getImageQualityScore()
?: throw NoFingerDetectedException("Failed to acquire image quality score")
validateMinimumFingerImageQuality(qualityScore)

// If the quality score is below the threshold and we don't allow low quality extraction, return an empty template
if (qualityScore < qualityThreshold && !allowLowQualityExtraction) {
AcquireFingerprintTemplateResponse(
ByteArray(0), templateFormat, qualityScore
)

} else {
acquireTemplateAndAssembleResponse(qualityScore)
?: throw NoFingerDetectedException("Failed to acquire template")
}
}

private fun Single<CaptureFingerprintResult>.ensureCaptureResultOkOrError() =
flatMapCompletable {
when (it) {
CaptureFingerprintResult.OK -> Completable.complete()
CaptureFingerprintResult.FINGERPRINT_NOT_FOUND -> Completable.error(
NoFingerDetectedException("Fingerprint not found")
)

CaptureFingerprintResult.DPI_UNSUPPORTED -> Completable.error(
UnexpectedScannerException("Capture fingerprint DPI unsupported")
)

CaptureFingerprintResult.UNKNOWN_ERROR -> Completable.error(
UnknownScannerIssueException("Unknown error when capturing fingerprint")
)
}
private fun CaptureFingerprintResult.ensureCaptureResultOkOrError() = when (this) {
CaptureFingerprintResult.OK -> { /* Do nothing */
}

private fun Single<Int>.validateMinimumFingerImageQuality() =
flatMap { qualityScore ->
if (qualityScore > NO_FINGER_IMAGE_QUALITY_THRESHOLD) {
Single.just(qualityScore)
} else {
Single.error(NoFingerDetectedException("Image quality score below detection threshold"))
}
}
CaptureFingerprintResult.FINGERPRINT_NOT_FOUND -> throw NoFingerDetectedException("Fingerprint not found")
CaptureFingerprintResult.DPI_UNSUPPORTED -> throw UnexpectedScannerException("Capture fingerprint DPI unsupported")
CaptureFingerprintResult.UNKNOWN_ERROR -> throw UnknownScannerIssueException("Unknown error when capturing fingerprint")
}

private fun Single<Int>.acquireTemplateAndAssembleResponse() =
flatMapMaybe { imageQuality ->
scannerV2.acquireTemplate()
.map { templateData ->
AcquireFingerprintTemplateResponse(
templateData.template, templateFormat, imageQuality
)
}
private fun validateMinimumFingerImageQuality(qualityScore: Int) {
if (qualityScore <= NO_FINGER_IMAGE_QUALITY_THRESHOLD) {
throw NoFingerDetectedException("Image quality score below detection threshold")
}
}


private fun Single<AcquireFingerprintTemplateResponse>.ifNoFingerDetectedThenSetBadScanLedState() =
onErrorResumeNext {
if (it is NoFingerDetectedException) {
scannerV2.setSmileLedState(scannerUiHelper.badScanLedState())
.andThen(Single.error(it))
} else {
Single.error(it)
}
private suspend fun acquireTemplateAndAssembleResponse(imageQuality: Int) =
scannerV2.acquireTemplate()?.template?.let { templateData ->
AcquireFingerprintTemplateResponse(
templateData, templateFormat, imageQuality
)
}


companion object {
private const val NO_FINGER_IMAGE_QUALITY_THRESHOLD =
10 // The image quality at which we decide a fingerprint wasn't detected
private val IMAGE_FORMAT = ImageFormatData.WSQ(15)
private const val MIN_CAPTURE_DPI = 500
private const val MAX_CAPTURE_DPI = 1700
}

private fun <T> Single<T>.wrapErrorsFromScanner() =
onErrorResumeNext { Single.error(wrapErrorFromScanner(it)) }

}
Loading