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 @@ -176,13 +176,14 @@ internal class FingerprintCaptureViewModel @Inject constructor(

private suspend fun initBioSdk() {
try {
bioSdkWrapper= resolveBioSdkWrapperUseCase()
bioSdkWrapper = resolveBioSdkWrapperUseCase()
bioSdkWrapper.initialize()
} catch (e: BioSdkException.BioSdkInitializationException) {
Simber.e(e)
_invalidLicense.send()
}
}

private fun launchReconnect() {
if (!state.isShowingConnectionScreen) {
updateState {
Expand Down Expand Up @@ -265,7 +266,7 @@ internal class FingerprintCaptureViewModel @Inject constructor(
bioSdkWrapper.scanningTimeoutMs +
if (isImageTransferRequired()) bioSdkWrapper.imageTransferTimeoutMs else 0

private fun isImageTransferRequired(): Boolean =
private fun isImageTransferRequired(): Boolean =
bioSdkConfiguration.vero2?.imageSavingStrategy?.isImageTransferRequired() ?: false &&
scannerManager.scanner.isImageTransferSupported()

Expand Down Expand Up @@ -504,6 +505,7 @@ internal class FingerprintCaptureViewModel @Inject constructor(
Simber.e(e)
handleNoFingerDetected()
}

else -> {
updateCaptureState { toNotCollected() }
Simber.e(e)
Expand Down Expand Up @@ -614,7 +616,12 @@ internal class FingerprintCaptureViewModel @Inject constructor(

private suspend fun saveImageIfExists(id: CaptureId, collectedFinger: CaptureState.Collected) {
val captureEventId = captureEventIds[id]
val imageRef = saveImage(bioSdkConfiguration.vero2!!, captureEventId, collectedFinger)
val imageRef = saveImage(
Comment thread
luhmirin-s marked this conversation as resolved.
vero2Configuration = bioSdkConfiguration.vero2!!,
finger = id.finger,
captureEventId = captureEventId,
collectedFinger = collectedFinger,
)
imageRefs[id] = imageRef
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.simprints.fingerprint.capture.usecase

import com.simprints.core.domain.fingerprint.IFingerIdentifier
import com.simprints.fingerprint.capture.exceptions.FingerprintUnexpectedException
import com.simprints.fingerprint.capture.extensions.deduceFileExtension
import com.simprints.fingerprint.capture.extensions.toInt
import com.simprints.fingerprint.capture.state.CaptureState
import com.simprints.infra.config.store.models.Vero2Configuration
import com.simprints.infra.events.SessionEventRepository
Expand All @@ -18,13 +20,16 @@ internal class SaveImageUseCase @Inject constructor(

suspend operator fun invoke(
vero2Configuration: Vero2Configuration,
finger: IFingerIdentifier,
captureEventId: String?,
collectedFinger: CaptureState.Collected,
) = if (collectedFinger.scanResult.image != null && captureEventId != null) {
saveImage(
Comment thread
luhmirin-s marked this conversation as resolved.
collectedFinger.scanResult.image,
captureEventId,
vero2Configuration.imageSavingStrategy.deduceFileExtension()
imageBytes = collectedFinger.scanResult.image,
captureEventId = captureEventId,
fileExtension = vero2Configuration.imageSavingStrategy.deduceFileExtension(),
finger = finger,
dpi = vero2Configuration.captureStrategy.toInt(),
)
} else if (collectedFinger.scanResult.image != null && captureEventId == null) {
Simber.e(FingerprintUnexpectedException("Could not save fingerprint image because of null capture ID"))
Expand All @@ -35,12 +40,22 @@ internal class SaveImageUseCase @Inject constructor(
imageBytes: ByteArray,
captureEventId: String,
fileExtension: String,
finger: IFingerIdentifier,
dpi: Int,
): SecuredImageRef? = determinePath(captureEventId, fileExtension)?.let { path ->
Simber.d("Saving fingerprint image ${path}")
val currentSession = coreEventRepository.getCurrentSessionScope()
val projectId = currentSession.projectId

val securedImageRef = coreImageRepository.storeImageSecurely(imageBytes, projectId, Path(path.parts))
val securedImageRef = coreImageRepository.storeImageSecurely(
imageBytes = imageBytes,
projectId = projectId,
relativePath = Path(path.parts),
metadata = mapOf(
META_KEY_FINGER_ID to finger.name,
META_KEY_DPI to dpi.toString(),
),
)

if (securedImageRef != null) {
SecuredImageRef(Path(securedImageRef.relativePath.parts))
Expand Down Expand Up @@ -70,5 +85,8 @@ internal class SaveImageUseCase @Inject constructor(

const val SESSIONS_PATH = "sessions"
const val FINGERPRINTS_PATH = "fingerprints"

private const val META_KEY_DPI = "dpi"
private const val META_KEY_FINGER_ID = "finger"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ class FingerprintCaptureViewModelTest {
)
coVerify(exactly = 12) { addCaptureEventsUseCase.invoke(any(), any(), any(), any()) }
vm.handleConfirmFingerprintsAndContinue()
coVerify(exactly = 4) { saveImageUseCase.invoke(any(), any(), any()) }
coVerify(exactly = 4) { saveImageUseCase.invoke(any(), any(), any(), any()) }

vm.finishWithFingerprints.assertEventReceivedWithContentAssertions { actualFingerprints ->
assertThat(actualFingerprints?.results).hasSize(FOUR_FINGERS_IDS.size)
Expand Down Expand Up @@ -489,7 +489,7 @@ class FingerprintCaptureViewModelTest {
coVerify(exactly = 2) { addCaptureEventsUseCase.invoke(any(), any(), any(), any()) }

vm.handleConfirmFingerprintsAndContinue()
coVerify(exactly = 2) { saveImageUseCase.invoke(any(), any(), any()) }
coVerify(exactly = 2) { saveImageUseCase.invoke(any(), any(), any(), any()) }

vm.finishWithFingerprints.assertEventReceivedWithContentAssertions { actualFingerprints ->
assertThat(actualFingerprints?.results).hasSize(TWO_FINGERS_IDS.size)
Expand Down Expand Up @@ -538,7 +538,7 @@ class FingerprintCaptureViewModelTest {
)
coVerify(exactly = 2) { addCaptureEventsUseCase.invoke(any(), any(), any(), any()) }
vm.handleConfirmFingerprintsAndContinue()
coVerify(exactly = 0) { saveImageUseCase.invoke(any(), any(), any()) }
coVerify(exactly = 0) { saveImageUseCase.invoke(any(), any(), any(), any()) }

vm.finishWithFingerprints.assertEventReceivedWithContentAssertions { actualFingerprints ->
assertThat(actualFingerprints?.results).hasSize(TWO_FINGERS_IDS.size)
Expand Down Expand Up @@ -736,7 +736,7 @@ class FingerprintCaptureViewModelTest {
coVerify(exactly = 14) { addCaptureEventsUseCase.invoke(any(), any(), any(), any()) }

vm.handleConfirmFingerprintsAndContinue()
coVerify(exactly = 3) { saveImageUseCase.invoke(any(), any(), any()) }
coVerify(exactly = 3) { saveImageUseCase.invoke(any(), any(), any(), any()) }

vm.finishWithFingerprints.assertEventReceivedWithContentAssertions { actualFingerprints ->
assertThat(actualFingerprints?.results).hasSize(3)
Expand Down Expand Up @@ -844,7 +844,7 @@ class FingerprintCaptureViewModelTest {

coVerify(exactly = 14) { addCaptureEventsUseCase.invoke(any(), any(), any(), any()) }
// If eager, expect that images were saved before confirm was pressed, including bad scans
coVerify(exactly = 8) { saveImageUseCase.invoke(any(), any(), any()) }
coVerify(exactly = 8) { saveImageUseCase.invoke(any(), any(), any(), any()) }

vm.handleConfirmFingerprintsAndContinue()

Expand Down Expand Up @@ -1234,7 +1234,7 @@ class FingerprintCaptureViewModelTest {

private fun withImageTransfer(isEager: Boolean = false) {
every { vero2Configuration.imageSavingStrategy } returns if (isEager) ImageSavingStrategy.EAGER else ImageSavingStrategy.ONLY_GOOD_SCAN
coEvery { saveImageUseCase.invoke(any(), any(), any()) } returns mockk {
coEvery { saveImageUseCase.invoke(any(), any(), any(), any()) } returns mockk {
every { relativePath } returns Path(emptyArray())
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.simprints.fingerprint.capture.usecase

import com.google.common.truth.Truth.assertThat
import com.simprints.core.domain.fingerprint.IFingerIdentifier
import com.simprints.fingerprint.capture.state.CaptureState
import com.simprints.fingerprint.capture.state.ScanResult
import com.simprints.infra.config.store.models.FingerprintConfiguration
Expand Down Expand Up @@ -38,6 +39,7 @@ class SaveImageUseCaseTest {
MockKAnnotations.init(this, relaxed = true)

every { vero2Configuration.imageSavingStrategy } returns Vero2Configuration.ImageSavingStrategy.EAGER
every { vero2Configuration.captureStrategy } returns Vero2Configuration.CaptureStrategy.SECUGEN_ISO_1300_DPI

useCase = SaveImageUseCase(imageRepo, eventRepo)
}
Expand All @@ -46,6 +48,7 @@ class SaveImageUseCaseTest {
fun `Returns null if no scan image`() = runTest {
val result = useCase.invoke(
vero2Configuration,
IFingerIdentifier.LEFT_3RD_FINGER,
"captureEventId",
createCollectedStub(null)
)
Expand All @@ -56,6 +59,7 @@ class SaveImageUseCaseTest {
fun `Returns null if no capture event id`() = runTest {
val result = useCase.invoke(
vero2Configuration,
IFingerIdentifier.LEFT_3RD_FINGER,
null,
createCollectedStub(byteArrayOf())
)
Expand All @@ -76,11 +80,12 @@ class SaveImageUseCaseTest {
"captureEventId.wsq"
))
coEvery {
imageRepo.storeImageSecurely(any(), "projectId", any())
imageRepo.storeImageSecurely(any(), "projectId", any(), any())
} returns SecuredImageRef(expectedPath)

assertThat(useCase.invoke(
vero2Configuration,
IFingerIdentifier.LEFT_3RD_FINGER,
"captureEventId",
createCollectedStub(byteArrayOf())
)).isNotNull()
Expand All @@ -91,7 +96,8 @@ class SaveImageUseCaseTest {
"projectId",
withArg {
assert(expectedPath.compose().contains(it.compose()))
}
},
any()
)
}
}
Expand All @@ -102,6 +108,7 @@ class SaveImageUseCaseTest {

assertThat(useCase.invoke(
vero2Configuration,
IFingerIdentifier.LEFT_3RD_FINGER,
"captureEventId",
createCollectedStub(byteArrayOf())
)).isNull()
Expand All @@ -114,16 +121,17 @@ class SaveImageUseCaseTest {
every { id } returns "sessionId"
}
coEvery {
imageRepo.storeImageSecurely(any(), "projectId", any())
imageRepo.storeImageSecurely(any(), "projectId", any(), any())
} returns null

assertThat(useCase.invoke(
vero2Configuration,
IFingerIdentifier.LEFT_3RD_FINGER,
"captureEventId",
createCollectedStub(byteArrayOf())
)).isNull()

coVerify { imageRepo.storeImageSecurely(any(), "projectId", any()) }
coVerify { imageRepo.storeImageSecurely(any(), "projectId", any(), any()) }
}

private fun createCollectedStub(image: ByteArray?) = CaptureState.Collected(
Expand Down
1 change: 1 addition & 0 deletions infra/images/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("simprints.infra")
id("simprints.library.room")
id("kotlin-parcelize")
id("simprints.testing.android")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ interface ImageRepository {
* @param imageBytes the image, in bytes
* @param projectId the id of the project
* @param relativePath the path of the image within the images root folder, including file name
* @param metadata arbitrary key-value pairs to be associated with the image
*
* @return a reference to the newly stored image, if successful, otherwise null
* @see [com.simprints.infra.images.local.ImageLocalDataSource.encryptAndStoreImage]
*/
suspend fun storeImageSecurely(
imageBytes: ByteArray,
projectId: String,
relativePath: Path
relativePath: Path,
metadata: Map<String, String> = emptyMap(),
): SecuredImageRef?

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
package com.simprints.infra.images

import com.simprints.infra.images.local.ImageLocalDataSource
import com.simprints.infra.images.metadata.ImageMetadataStore
import com.simprints.infra.images.model.Path
import com.simprints.infra.images.model.SecuredImageRef
import com.simprints.infra.images.remote.ImageRemoteDataSource
import com.simprints.infra.logging.Simber
import javax.inject.Inject

class ImageRepositoryImpl @Inject internal constructor(
internal class ImageRepositoryImpl @Inject internal constructor(
private val localDataSource: ImageLocalDataSource,
private val remoteDataSource: ImageRemoteDataSource,
private val metadataStore: ImageMetadataStore,
) : ImageRepository {

override suspend fun storeImageSecurely(
imageBytes: ByteArray,
projectId: String,
relativePath: Path,
): SecuredImageRef? = localDataSource.encryptAndStoreImage(imageBytes, projectId, relativePath)
metadata: Map<String, String>,
): SecuredImageRef? {
return localDataSource.encryptAndStoreImage(imageBytes, projectId, relativePath)
// Only store metadata if the image was stored successfully
?.also { metadataStore.storeMetadata(relativePath, metadata) }
}

override suspend fun getNumberOfImagesToUpload(projectId: String): Int =
localDataSource.listImages(projectId).count()
Expand All @@ -29,9 +36,11 @@ class ImageRepositoryImpl @Inject internal constructor(
images.forEach { imageRef ->
try {
localDataSource.decryptImage(imageRef)?.let { stream ->
val uploadResult = remoteDataSource.uploadImage(stream, imageRef)
val metadata = metadataStore.getMetadata(imageRef.relativePath)
val uploadResult = remoteDataSource.uploadImage(stream, imageRef, metadata)
if (uploadResult.isUploadSuccessful()) {
localDataSource.deleteImage(imageRef)
metadataStore.deleteMetadata(imageRef.relativePath)
} else {
allImagesUploaded = false
Simber.e("Failed to upload image without exception")
Expand All @@ -47,6 +56,7 @@ class ImageRepositoryImpl @Inject internal constructor(
}

override suspend fun deleteStoredImages() {
metadataStore.deleteAllMetadata()
for (image in localDataSource.listImages(null)) {
localDataSource.deleteImage(image)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.simprints.infra.images

import android.content.Context
import com.simprints.infra.images.local.ImageLocalDataSource
import com.simprints.infra.images.local.ImageLocalDataSourceImpl
import com.simprints.infra.images.metadata.database.ImageMetadataDao
import com.simprints.infra.images.metadata.database.ImageMetadataDatabase
import com.simprints.infra.images.remote.ImageRemoteDataSource
import com.simprints.infra.images.remote.ImageRemoteDataSourceImpl
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
Expand All @@ -23,3 +29,16 @@ abstract class ImagesModule {
internal abstract fun bindImageRemoteDataSource(impl: ImageRemoteDataSourceImpl): ImageRemoteDataSource

}

@Module
@InstallIn(SingletonComponent::class)
internal class ImageModuleProviders {

@Provides
@Singleton
fun provideImageMetadataDatabase(@ApplicationContext ctx: Context): ImageMetadataDatabase = ImageMetadataDatabase.getDatabase(ctx)

@Provides
@Singleton
fun provideImageMetadataDao(database: ImageMetadataDatabase): ImageMetadataDao = database.imageMetadataDao
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.simprints.infra.images.metadata

import com.simprints.infra.images.metadata.database.DbImageMetadata
import com.simprints.infra.images.metadata.database.ImageMetadataDao
import com.simprints.infra.images.model.Path
import javax.inject.Inject

internal class ImageMetadataStore @Inject constructor(
private val imageMetadataDao: ImageMetadataDao,
) {

suspend fun storeMetadata(imageKey: Path, metadata: Map<String, String>) = metadata
.takeIf { it.isNotEmpty() }
?.map { (k, v) -> DbImageMetadata(imageId = extractKey(imageKey), key = k, value = v) }
?.let { imageMetadataDao.save(it) }

suspend fun getMetadata(imageKey: Path): Map<String, String> = imageMetadataDao
.get(extractKey(imageKey))
.let { metadata -> metadata.associate { it.key to it.value } }

suspend fun deleteMetadata(imageKey: Path) = imageMetadataDao.delete(extractKey(imageKey))

suspend fun deleteAllMetadata() = imageMetadataDao.deleteAll()

// Using only the file name as the key to avoid confusion with the path root
private fun extractKey(key: Path) = key.parts.last()
}
Loading