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,17 @@ 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.flow.filterNot
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
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 @@ -39,14 +43,23 @@ class ObserveFingerprintScanStatusUseCase @Inject constructor(
ledsMode = configManager.getProjectConfiguration().fingerprint?.getSdkConfiguration(
fingerprintSdk
)?.vero2?.ledsMode
statusTracker.state.collect { state ->
provideFeedback(state)
previousState = state
}

statusTracker.state
.onEach { state ->
if (state is FingerprintScanState.ScanCompleted) {
provideFeedback(state)// Handle ScanCompleted immediately to let the user know to remove the finger
}
}
.filterNot { it is FingerprintScanState.ScanCompleted } // Exclude ScanCompleted
.flatMapConcat { state ->
flow { emit(provideFeedback(state)) } // Process other states sequentially
}
.collect {
// Do nothing
}
}
}


private suspend fun provideFeedback(state: FingerprintScanState) {
when (state) {
is FingerprintScanState.Idle -> turnOnFlashingWhiteSmileLeds()
Expand All @@ -55,6 +68,7 @@ class ObserveFingerprintScanStatusUseCase @Inject constructor(
is FingerprintScanState.ImageQualityChecking.Good -> setUiAfterScan(true)
is FingerprintScanState.ImageQualityChecking.Bad -> setUiAfterScan(false)
}
previousState = state
}

fun stopObserving() {
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
Comment thread
meladRaouf marked this conversation as resolved.
)
}


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)) }

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retry
import kotlinx.coroutines.rx2.await
import kotlinx.coroutines.withContext
import java.io.IOException
import java.util.UUID
Expand Down Expand Up @@ -91,29 +90,26 @@ internal class ConnectionHelper @Inject constructor(
withContext(dispatcher) { socket.connect() }
this.socket = socket
return socket
} catch (e: IOException) {
} catch (_: IOException) {
throw ScannerDisconnectedException()
}
}

private suspend fun connectScannerObjectWithSocket(
scanner: Scanner,
socket: ComponentBluetoothSocket
) = withContext(dispatcher) {
private fun connectScannerObjectWithSocket(
scanner: Scanner, socket: ComponentBluetoothSocket
) {
Simber.d("Socket connected. Setting up scanner...")
scanner.connect(socket.getInputStream(), socket.getOutputStream()).await()
scanner.connect(socket.getInputStream(), socket.getOutputStream())
}

suspend fun disconnectScanner(scanner: Scanner): Unit = withContext(dispatcher) {
scanner.disconnect().await()
fun disconnectScanner(scanner: Scanner) {
scanner.disconnect()
socket?.close()
}

suspend fun reconnect(
scanner: Scanner,
macAddress: String,
maxRetries: Long = CONNECT_MAX_RETRIES
)= withContext(dispatcher) {
scanner: Scanner, macAddress: String, maxRetries: Long = CONNECT_MAX_RETRIES
) {
disconnectScanner(scanner)
delay(RECONNECT_DELAY_MS)
connectScanner(scanner, macAddress, maxRetries).collect()
Expand Down
Loading