From 6bf8023931d45ac9562d5574174e2459634d7468 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Thu, 25 Apr 2024 10:22:50 +0300 Subject: [PATCH 1/8] MS-337 Add metadata to image file upload --- .../simprints/infra/images/ImageRepository.kt | 5 +++- .../infra/images/ImageRepositoryImpl.kt | 11 ++++++-- .../images/remote/ImageRemoteDataSource.kt | 4 ++- .../remote/ImageRemoteDataSourceImpl.kt | 12 ++++++-- .../infra/images/ImageRepositoryImplTest.kt | 4 +-- .../remote/ImageRemoteDataSourceImplTest.kt | 28 +++++++++++++++++-- 6 files changed, 53 insertions(+), 11 deletions(-) diff --git a/infra/images/src/main/java/com/simprints/infra/images/ImageRepository.kt b/infra/images/src/main/java/com/simprints/infra/images/ImageRepository.kt index 62a228ae07..4b2b9af2e9 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/ImageRepository.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/ImageRepository.kt @@ -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 = emptyMap(), ): SecuredImageRef? /** diff --git a/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt b/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt index 6db5067cda..60f8ebf8a5 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt @@ -16,7 +16,12 @@ class ImageRepositoryImpl @Inject internal constructor( imageBytes: ByteArray, projectId: String, relativePath: Path, - ): SecuredImageRef? = localDataSource.encryptAndStoreImage(imageBytes, projectId, relativePath) + metadata: Map, + ): SecuredImageRef? { + // TODO store metadata + + return localDataSource.encryptAndStoreImage(imageBytes, projectId, relativePath) + } override suspend fun getNumberOfImagesToUpload(projectId: String): Int = localDataSource.listImages(projectId).count() @@ -29,7 +34,9 @@ class ImageRepositoryImpl @Inject internal constructor( images.forEach { imageRef -> try { localDataSource.decryptImage(imageRef)?.let { stream -> - val uploadResult = remoteDataSource.uploadImage(stream, imageRef) + val metadata = emptyMap() // TODO fetch metadata + + val uploadResult = remoteDataSource.uploadImage(stream, imageRef, metadata) if (uploadResult.isUploadSuccessful()) { localDataSource.deleteImage(imageRef) } else { diff --git a/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSource.kt b/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSource.kt index 9e44056f42..ff76839019 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSource.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSource.kt @@ -13,13 +13,15 @@ internal interface ImageRemoteDataSource { * * @param imageStream the image file as a stream * @param imageRef a reference to the image to be uploaded + * @param metadata arbitrary key-value pairs to be associated with the image * * @return the result of the operation. * @see [UploadResult] */ suspend fun uploadImage( imageStream: FileInputStream, - imageRef: SecuredImageRef + imageRef: SecuredImageRef, + metadata: Map, ): UploadResult } diff --git a/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImpl.kt b/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImpl.kt index 499fe1969d..441cbfd3a1 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImpl.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImpl.kt @@ -1,6 +1,7 @@ package com.simprints.infra.images.remote import com.google.firebase.storage.FirebaseStorage +import com.google.firebase.storage.StorageMetadata import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.images.model.SecuredImageRef @@ -17,6 +18,7 @@ internal class ImageRemoteDataSourceImpl @Inject constructor( override suspend fun uploadImage( imageStream: FileInputStream, imageRef: SecuredImageRef, + metadata: Map, ): UploadResult { val firebaseProjectName = authStore.getLegacyAppFallback().options.projectId @@ -43,7 +45,14 @@ internal class ImageRemoteDataSourceImpl @Inject constructor( Simber.d("Uploading ${fileRef.path}") - val uploadTask = fileRef.putStream(imageStream).await() + val uploadTask = if (metadata.isEmpty()) { + fileRef.putStream(imageStream).await() + } else { + val storeMetadata = StorageMetadata.Builder() + .also { metadata.forEach { (key, value) -> it.setCustomMetadata(key, value) } } + .build() + fileRef.putStream(imageStream, storeMetadata).await() + } val status = if (uploadTask.task.isSuccessful) { UploadResult.Status.SUCCESSFUL @@ -58,5 +67,4 @@ internal class ImageRemoteDataSourceImpl @Inject constructor( } } - } diff --git a/infra/images/src/test/java/com/simprints/infra/images/ImageRepositoryImplTest.kt b/infra/images/src/test/java/com/simprints/infra/images/ImageRepositoryImplTest.kt index 05531619aa..8541dd097b 100644 --- a/infra/images/src/test/java/com/simprints/infra/images/ImageRepositoryImplTest.kt +++ b/infra/images/src/test/java/com/simprints/infra/images/ImageRepositoryImplTest.kt @@ -132,7 +132,7 @@ internal class ImageRepositoryImplTest { } returns mockStream coEvery { - remoteDataSource.uploadImage(mockStream, validImage) + remoteDataSource.uploadImage(mockStream, validImage, emptyMap()) } returns UploadResult( validImage, UploadResult.Status.SUCCESSFUL @@ -148,7 +148,7 @@ internal class ImageRepositoryImplTest { } returns mockStream coEvery { - remoteDataSource.uploadImage(mockStream, invalidImage) + remoteDataSource.uploadImage(mockStream, invalidImage, emptyMap()) } returns UploadResult( invalidImage, UploadResult.Status.FAILED diff --git a/infra/images/src/test/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImplTest.kt b/infra/images/src/test/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImplTest.kt index 2c6b626145..390ae07226 100644 --- a/infra/images/src/test/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImplTest.kt +++ b/infra/images/src/test/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImplTest.kt @@ -69,7 +69,22 @@ class ImageRemoteDataSourceImplTest { every { FirebaseStorage.getInstance(any(), any()) } returns storageMock - val result = remoteDataSource.uploadImage(mockImageStream, mockSecuredImageRef) + val result = remoteDataSource.uploadImage(mockImageStream, mockSecuredImageRef, emptyMap()) + + assertThat(result.isUploadSuccessful()).isTrue() + } + + @Test + fun `test image with metadata upload flow`() = runTest { + coEvery { configManager.getProject(any()).imageBucket } returns "gs://`simprints-dev.appspot.com" + every { authStore.getLegacyAppFallback().options.projectId } returns "projectId" + every { authStore.signedInProjectId } returns "projectId" + + val storageMock = setupStorageMock() + + every { FirebaseStorage.getInstance(any(), any()) } returns storageMock + + val result = remoteDataSource.uploadImage(mockImageStream, mockSecuredImageRef, mapOf("key" to "value")) assertThat(result.isUploadSuccessful()).isTrue() } @@ -78,7 +93,7 @@ class ImageRemoteDataSourceImplTest { fun `null project returns failed upload`() = runTest { every { authStore.getLegacyAppFallback().options.projectId } returns null - val result = remoteDataSource.uploadImage(mockImageStream, mockSecuredImageRef) + val result = remoteDataSource.uploadImage(mockImageStream, mockSecuredImageRef, emptyMap()) assertThat(result.isUploadSuccessful()).isFalse() } @@ -88,7 +103,7 @@ class ImageRemoteDataSourceImplTest { coEvery { configManager.getProject(any()).imageBucket } returns "" every { authStore.getLegacyAppFallback().options.projectId } returns "projectId" - val result = remoteDataSource.uploadImage(mockImageStream, mockSecuredImageRef) + val result = remoteDataSource.uploadImage(mockImageStream, mockSecuredImageRef, emptyMap()) assertThat(result.isUploadSuccessful()).isFalse() } @@ -103,6 +118,13 @@ class ImageRemoteDataSourceImplTest { } } } + every { putStream(any(), any()) } returns mockk { + coEvery { await() } returns mockk { + every { task } returns mockk { + every { isSuccessful } returns true + } + } + } } } From 111d15638697ec1248eb32170404fad3d4c19c97 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Thu, 25 Apr 2024 10:33:52 +0300 Subject: [PATCH 2/8] MS-337 Pass fingerprint image metadata when saving image --- .../screen/FingerprintCaptureViewModel.kt | 13 +++++++--- .../capture/usecase/SaveImageUseCase.kt | 26 ++++++++++++++++--- .../screen/FingerprintCaptureViewModelTest.kt | 12 ++++----- .../capture/usecase/SaveImageUseCaseTest.kt | 16 +++++++++--- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt index b89c832f5d..a40429cced 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt @@ -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 { @@ -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() @@ -504,6 +505,7 @@ internal class FingerprintCaptureViewModel @Inject constructor( Simber.e(e) handleNoFingerDetected() } + else -> { updateCaptureState { toNotCollected() } Simber.e(e) @@ -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( + vero2Configuration = bioSdkConfiguration.vero2!!, + finger = id.finger, + captureEventId = captureEventId, + collectedFinger = collectedFinger, + ) imageRefs[id] = imageRef } diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/usecase/SaveImageUseCase.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/usecase/SaveImageUseCase.kt index 9843724593..ec4b2b0f0e 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/usecase/SaveImageUseCase.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/usecase/SaveImageUseCase.kt @@ -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 @@ -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( - 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")) @@ -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)) @@ -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" } } diff --git a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt index ea54548bcf..c432a0ff4e 100644 --- a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt +++ b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt @@ -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) @@ -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) @@ -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) @@ -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) @@ -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() @@ -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()) } } diff --git a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/usecase/SaveImageUseCaseTest.kt b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/usecase/SaveImageUseCaseTest.kt index 06455e664f..9ce4139714 100644 --- a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/usecase/SaveImageUseCaseTest.kt +++ b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/usecase/SaveImageUseCaseTest.kt @@ -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 @@ -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) } @@ -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) ) @@ -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()) ) @@ -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() @@ -91,7 +96,8 @@ class SaveImageUseCaseTest { "projectId", withArg { assert(expectedPath.compose().contains(it.compose())) - } + }, + any() ) } } @@ -102,6 +108,7 @@ class SaveImageUseCaseTest { assertThat(useCase.invoke( vero2Configuration, + IFingerIdentifier.LEFT_3RD_FINGER, "captureEventId", createCollectedStub(byteArrayOf()) )).isNull() @@ -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( From 11758b95b066577d8a528abb94dc0beca09152fa Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Thu, 25 Apr 2024 11:24:15 +0300 Subject: [PATCH 3/8] MS-337 Introduce metadata database --- infra/images/build.gradle.kts | 1 + .../infra/images/ImageRepositoryImpl.kt | 13 +-- .../simprints/infra/images/ImagesModule.kt | 19 ++++ .../images/metadata/ImageMetadataStore.kt | 27 ++++++ .../metadata/database/DbImageMetadata.kt | 15 ++++ .../metadata/database/ImageMetadataDao.kt | 22 +++++ .../database/ImageMetadataDatabase.kt | 30 +++++++ .../infra/images/ImageRepositoryImplTest.kt | 16 +++- .../images/metadata/ImageMetadataStoreTest.kt | 89 +++++++++++++++++++ 9 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 infra/images/src/main/java/com/simprints/infra/images/metadata/ImageMetadataStore.kt create mode 100644 infra/images/src/main/java/com/simprints/infra/images/metadata/database/DbImageMetadata.kt create mode 100644 infra/images/src/main/java/com/simprints/infra/images/metadata/database/ImageMetadataDao.kt create mode 100644 infra/images/src/main/java/com/simprints/infra/images/metadata/database/ImageMetadataDatabase.kt create mode 100644 infra/images/src/test/java/com/simprints/infra/images/metadata/ImageMetadataStoreTest.kt diff --git a/infra/images/build.gradle.kts b/infra/images/build.gradle.kts index bf6740850b..5ed3944f35 100644 --- a/infra/images/build.gradle.kts +++ b/infra/images/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("simprints.infra") + id("simprints.library.room") id("kotlin-parcelize") id("simprints.testing.android") } diff --git a/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt b/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt index 60f8ebf8a5..a4133f1bfc 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt @@ -1,15 +1,17 @@ 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( @@ -18,9 +20,9 @@ class ImageRepositoryImpl @Inject internal constructor( relativePath: Path, metadata: Map, ): SecuredImageRef? { - // TODO store metadata - 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 = @@ -34,11 +36,11 @@ class ImageRepositoryImpl @Inject internal constructor( images.forEach { imageRef -> try { localDataSource.decryptImage(imageRef)?.let { stream -> - val metadata = emptyMap() // TODO fetch metadata - + 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") @@ -54,6 +56,7 @@ class ImageRepositoryImpl @Inject internal constructor( } override suspend fun deleteStoredImages() { + metadataStore.deleteAllMetadata() for (image in localDataSource.listImages(null)) { localDataSource.deleteImage(image) } diff --git a/infra/images/src/main/java/com/simprints/infra/images/ImagesModule.kt b/infra/images/src/main/java/com/simprints/infra/images/ImagesModule.kt index 8a45f9cfd3..2f923f49d3 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/ImagesModule.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/ImagesModule.kt @@ -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) @@ -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 +} diff --git a/infra/images/src/main/java/com/simprints/infra/images/metadata/ImageMetadataStore.kt b/infra/images/src/main/java/com/simprints/infra/images/metadata/ImageMetadataStore.kt new file mode 100644 index 0000000000..a539a6c6ba --- /dev/null +++ b/infra/images/src/main/java/com/simprints/infra/images/metadata/ImageMetadataStore.kt @@ -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) = 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 = 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() +} diff --git a/infra/images/src/main/java/com/simprints/infra/images/metadata/database/DbImageMetadata.kt b/infra/images/src/main/java/com/simprints/infra/images/metadata/database/DbImageMetadata.kt new file mode 100644 index 0000000000..81e2b3ff8c --- /dev/null +++ b/infra/images/src/main/java/com/simprints/infra/images/metadata/database/DbImageMetadata.kt @@ -0,0 +1,15 @@ +package com.simprints.infra.images.metadata.database + +import androidx.room.Entity +import com.google.errorprone.annotations.Keep + +@Entity( + tableName = "DbImageMetadata", + primaryKeys = ["imageId", "key"], +) +@Keep +internal data class DbImageMetadata( + val imageId: String, + val key: String, + val value: String, +) diff --git a/infra/images/src/main/java/com/simprints/infra/images/metadata/database/ImageMetadataDao.kt b/infra/images/src/main/java/com/simprints/infra/images/metadata/database/ImageMetadataDao.kt new file mode 100644 index 0000000000..efaeff1383 --- /dev/null +++ b/infra/images/src/main/java/com/simprints/infra/images/metadata/database/ImageMetadataDao.kt @@ -0,0 +1,22 @@ +package com.simprints.infra.images.metadata.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +internal interface ImageMetadataDao { + + @Query("SELECT * FROM DbImageMetadata WHERE imageId = :imageId") + suspend fun get(imageId: String): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(metadata: List) + + @Query("DELETE FROM DbImageMetadata WHERE imageId = :imageId") + suspend fun delete(imageId: String) + + @Query("DELETE FROM DbImageMetadata") + suspend fun deleteAll() +} diff --git a/infra/images/src/main/java/com/simprints/infra/images/metadata/database/ImageMetadataDatabase.kt b/infra/images/src/main/java/com/simprints/infra/images/metadata/database/ImageMetadataDatabase.kt new file mode 100644 index 0000000000..195dfc9146 --- /dev/null +++ b/infra/images/src/main/java/com/simprints/infra/images/metadata/database/ImageMetadataDatabase.kt @@ -0,0 +1,30 @@ +package com.simprints.infra.images.metadata.database + +import android.content.Context +import androidx.room.* +import com.google.errorprone.annotations.Keep + +@Database( + entities = [DbImageMetadata::class], + version = 1, + exportSchema = false +) +@Keep +internal abstract class ImageMetadataDatabase : RoomDatabase() { + + abstract val imageMetadataDao: ImageMetadataDao + + companion object { + + private const val ROOM_DB_NAME = "image_meta_db" + + fun getDatabase(context: Context): ImageMetadataDatabase = Room + .databaseBuilder( + context.applicationContext, + ImageMetadataDatabase::class.java, + ROOM_DB_NAME + ) + .fallbackToDestructiveMigration() + .build() + } +} diff --git a/infra/images/src/test/java/com/simprints/infra/images/ImageRepositoryImplTest.kt b/infra/images/src/test/java/com/simprints/infra/images/ImageRepositoryImplTest.kt index 8541dd097b..13448edb8b 100644 --- a/infra/images/src/test/java/com/simprints/infra/images/ImageRepositoryImplTest.kt +++ b/infra/images/src/test/java/com/simprints/infra/images/ImageRepositoryImplTest.kt @@ -2,6 +2,7 @@ package com.simprints.infra.images import com.google.common.truth.Truth.assertThat 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 @@ -9,8 +10,10 @@ import com.simprints.infra.images.remote.UploadResult import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.impl.annotations.MockK +import io.mockk.justRun import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Before @@ -25,15 +28,19 @@ internal class ImageRepositoryImplTest { @MockK lateinit var localDataSource: ImageLocalDataSource + @MockK lateinit var remoteDataSource: ImageRemoteDataSource + @MockK + lateinit var metadataStore: ImageMetadataStore + private lateinit var repository: ImageRepository @Before fun setUp() { MockKAnnotations.init(this) - repository = ImageRepositoryImpl(localDataSource, remoteDataSource) + repository = ImageRepositoryImpl(localDataSource, remoteDataSource, metadataStore) initValidImageMocks() } @@ -88,6 +95,7 @@ internal class ImageRepositoryImplTest { coVerify(exactly = 3) { localDataSource.decryptImage(any()) } coVerify(exactly = 3) { localDataSource.deleteImage(any()) } + coVerify(exactly = 3) { metadataStore.deleteMetadata(any()) } assertThat(successful).isTrue() } @@ -107,6 +115,7 @@ internal class ImageRepositoryImplTest { repository.deleteStoredImages() coVerify(exactly = 5) { localDataSource.deleteImage(any()) } + coVerify(exactly = 1) { metadataStore.deleteAllMetadata() } } @Test @@ -131,6 +140,11 @@ internal class ImageRepositoryImplTest { localDataSource.decryptImage(validImage) } returns mockStream + coJustRun { metadataStore.storeMetadata(any(), any()) } + coEvery { metadataStore.getMetadata(any()) } returns emptyMap() + coJustRun { metadataStore.deleteMetadata(any()) } + coJustRun { metadataStore.deleteAllMetadata() } + coEvery { remoteDataSource.uploadImage(mockStream, validImage, emptyMap()) } returns UploadResult( diff --git a/infra/images/src/test/java/com/simprints/infra/images/metadata/ImageMetadataStoreTest.kt b/infra/images/src/test/java/com/simprints/infra/images/metadata/ImageMetadataStoreTest.kt new file mode 100644 index 0000000000..ec046d4cf6 --- /dev/null +++ b/infra/images/src/test/java/com/simprints/infra/images/metadata/ImageMetadataStoreTest.kt @@ -0,0 +1,89 @@ +package com.simprints.infra.images.metadata + +import com.google.common.truth.Truth.assertThat +import com.simprints.infra.images.metadata.database.DbImageMetadata +import com.simprints.infra.images.metadata.database.ImageMetadataDao +import com.simprints.infra.images.model.Path +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class ImageMetadataStoreTest { + + @MockK + private lateinit var metadataDao: ImageMetadataDao + + private lateinit var metadataStore: ImageMetadataStore + + @Before + fun setUp() { + MockKAnnotations.init(this) + + metadataStore = ImageMetadataStore(metadataDao) + } + + @Test + fun `store metadata`() = runTest { + coJustRun { metadataDao.save(any()) } + + metadataStore.storeMetadata( + Path("path/imageKey"), + mapOf("key1" to "value1", "key2" to "value2") + ) + + coVerify { + metadataDao.save( + listOf( + DbImageMetadata("imageKey", "key1", "value1"), + DbImageMetadata("imageKey", "key2", "value2") + ) + ) + } + } + + @Test + fun `store empty metadata`() = runTest { + metadataStore.storeMetadata(Path("path/imageKey"), emptyMap()) + + coVerify(exactly = 0) { metadataDao.save(any()) } + } + + @Test + fun `getting metadata`() = runTest { + coEvery { metadataDao.get("imageKey") } returns listOf( + DbImageMetadata("imageKey", "key1", "value1"), + DbImageMetadata("imageKey", "key2", "value2") + ) + + assertThat(metadataStore.getMetadata(Path("imageKey"))) + .containsExactly("key1", "value1", "key2", "value2") + } + + @Test + fun `getting empty metadata`() = runTest { + coEvery { metadataDao.get("imageKey") } returns emptyList() + + assertThat(metadataStore.getMetadata(Path("imageKey"))).isEmpty() + } + + @Test + fun `delete metadata`() = runTest { + coJustRun { metadataDao.delete(any()) } + + metadataStore.deleteMetadata(Path("imageKey")) + coVerify { metadataDao.delete("imageKey") } + } + + @Test + fun `delete all metadata`() = runTest { + coJustRun { metadataDao.deleteAll() } + + metadataStore.deleteAllMetadata() + coVerify { metadataDao.deleteAll() } + } +} From 3f500b32bd9ff5270f9e5c96ddf9cad82fe0291c Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Tue, 7 May 2024 17:13:41 +0300 Subject: [PATCH 4/8] MS-438 Add capture event ID to the step result data --- .../face/capture/FaceCaptureResult.kt | 1 + .../capture/screens/FaceCaptureViewModel.kt | 15 ++++---- .../cache/OrchestratorCacheIntegrationTest.kt | 2 + ...apStepsForLastBiometricEnrolUseCaseTest.kt | 38 +++++++++++-------- .../capture/FingerprintCaptureResult.kt | 1 + .../screen/FingerprintCaptureViewModel.kt | 2 + 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt index aa0ac896d7..649d24b57a 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt @@ -11,6 +11,7 @@ data class FaceCaptureResult( @Keep data class Item( + val captureEventId: String?, val index: Int, val sample: Sample?, ) : Serializable diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt index 65a68554ef..cacbcc9247 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt @@ -62,7 +62,6 @@ internal class FaceCaptureViewModel @Inject constructor( get() = _finishFlowEvent private val _finishFlowEvent = MutableLiveData>() - val invalidLicense: LiveData get() = _invalidLicense private val _invalidLicense = MutableLiveData() @@ -106,12 +105,13 @@ internal class FaceCaptureViewModel @Inject constructor( val items = faceDetections.mapIndexed { index, detection -> FaceCaptureResult.Item( - index, - FaceCaptureResult.Sample( - detection.id, - detection.face?.template ?: ByteArray(0), - detection.securedImageRef, - detection.face?.format ?: "", + captureEventId = detection.id, + index = index, + sample = FaceCaptureResult.Sample( + faceId = detection.id, + template = detection.face?.template ?: ByteArray(0), + imageRef = detection.securedImageRef, + format = detection.face?.format ?: "", ) ) } @@ -159,5 +159,4 @@ internal class FaceCaptureViewModel @Inject constructor( eventReporter.addCaptureConfirmationEvent(startTime, isContinue) } - } diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt index fd5ea9f1ff..6f195bdccf 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt @@ -10,6 +10,7 @@ import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepId import com.simprints.feature.orchestrator.steps.StepStatus import com.simprints.fingerprint.capture.FingerprintCaptureResult +import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 import com.simprints.infra.security.SecurityManager import io.mockk.MockKAnnotations import io.mockk.every @@ -62,6 +63,7 @@ class OrchestratorCacheIntegrationTest { result = FingerprintCaptureResult( results = listOf( FingerprintCaptureResult.Item( + captureEventId = GUID1, identifier = IFingerIdentifier.LEFT_THUMB, sample = null ) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt index 9ff8a7ed4f..ab36dd7356 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt @@ -11,6 +11,7 @@ import com.simprints.infra.config.store.models.Finger import com.simprints.matcher.FaceMatchResult import com.simprints.matcher.FingerprintMatchResult import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 import org.junit.Before import org.junit.Test import java.io.Serializable @@ -48,13 +49,17 @@ internal class MapStepsForLastBiometricEnrolUseCaseTest { val result = useCase(listOf( FaceCaptureResult( results = listOf( - FaceCaptureResult.Item(0, null), - FaceCaptureResult.Item(0, FaceCaptureResult.Sample( - faceId = "faceId", - template = byteArrayOf(), - imageRef = null, - format = "format" - )) + FaceCaptureResult.Item(captureEventId = null, index = 0, sample = null), + FaceCaptureResult.Item( + captureEventId = GUID1, + index = 0, + sample = FaceCaptureResult.Sample( + faceId = "faceId", + template = byteArrayOf(), + imageRef = null, + format = "format" + ) + ) ), ) )) @@ -81,15 +86,18 @@ internal class MapStepsForLastBiometricEnrolUseCaseTest { val result = useCase(listOf( FingerprintCaptureResult( results = listOf( - FingerprintCaptureResult.Item(IFingerIdentifier.LEFT_THUMB, null), + FingerprintCaptureResult.Item(null, IFingerIdentifier.LEFT_THUMB, null), FingerprintCaptureResult.Item( - IFingerIdentifier.RIGHT_THUMB, FingerprintCaptureResult.Sample( - fingerIdentifier = IFingerIdentifier.RIGHT_THUMB, - template = byteArrayOf(), - templateQualityScore = 0, - imageRef = null, - format = "format" - )) + identifier = IFingerIdentifier.RIGHT_THUMB, + captureEventId = GUID1, + sample = FingerprintCaptureResult.Sample( + fingerIdentifier = IFingerIdentifier.RIGHT_THUMB, + template = byteArrayOf(), + templateQualityScore = 0, + imageRef = null, + format = "format" + ) + ) ), ) )) diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt index e7d245e820..5d430ea6e8 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt @@ -12,6 +12,7 @@ data class FingerprintCaptureResult( @Keep data class Item( + val captureEventId: String?, val identifier: IFingerIdentifier, val sample: Sample?, ) : Serializable diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt index b89c832f5d..fbea0da748 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt @@ -53,6 +53,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.util.UUID import javax.inject.Inject import kotlin.math.min @@ -600,6 +601,7 @@ internal class FingerprintCaptureViewModel @Inject constructor( val resultItems = collectedFingers.map { (captureId, collectedFinger) -> FingerprintCaptureResult.Item( identifier = captureId.finger, + captureEventId = captureEventIds[captureId], sample = FingerprintCaptureResult.Sample( fingerIdentifier = captureId.finger, template = collectedFinger.scanResult.template, From 20285c35f47a184b38a2e267a13710467cf0387d Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Tue, 7 May 2024 17:14:27 +0300 Subject: [PATCH 5/8] MS-438 Compose CreatePerson event only based on information from step results --- .../usecases/CreatePersonEventUseCase.kt | 81 +++++++------------ .../usecases/CreatePersonEventUseCaseTest.kt | 58 +++++++------ .../sync/down/tasks/SubjectFactoryTest.kt | 3 + 3 files changed, 60 insertions(+), 82 deletions(-) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCase.kt index f14eea7b11..e6b2e01bc9 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCase.kt @@ -5,20 +5,16 @@ import com.simprints.core.domain.face.uniqueId import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.uniqueId import com.simprints.core.tools.time.TimeHelper -import com.simprints.core.tools.utils.EncodingUtils import com.simprints.face.capture.FaceCaptureResult import com.simprints.fingerprint.capture.FingerprintCaptureResult import com.simprints.infra.events.SessionEventRepository import com.simprints.infra.events.event.domain.models.PersonCreationEvent -import com.simprints.infra.events.event.domain.models.face.FaceCaptureBiometricsEvent -import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent import java.io.Serializable import javax.inject.Inject internal class CreatePersonEventUseCase @Inject constructor( private val eventRepository: SessionEventRepository, private val timeHelper: TimeHelper, - private val encodingUtils: EncodingUtils, ) { suspend operator fun invoke(results: List) { @@ -27,70 +23,53 @@ internal class CreatePersonEventUseCase @Inject constructor( // If a personCreationEvent is already in the current session, // we don' want to add it again (the capture steps would still be the same) if (sessionEvents.none { it is PersonCreationEvent }) { - val faceCaptureBiometricsEvents = sessionEvents.filterIsInstance() - val fingerprintCaptureBiometricsEvents = sessionEvents.filterIsInstance() + val faceCaptures = extractFaceCaptures(results) + val fingerprintCaptures = extractFingerprintCaptures(results) - val faceSamples = extractFaceSamples(results) - val fingerprintSamples = extractFingerprintSamples(results) + val personCreationEvent = build(faceCaptures, fingerprintCaptures) - val personCreationEvent = build( - faceCaptureBiometricsEvents, - fingerprintCaptureBiometricsEvents, - faceSamples, - fingerprintSamples - ) if (personCreationEvent.hasBiometricData()) { eventRepository.addOrUpdateEvent(personCreationEvent) } } } - private fun extractFaceSamples(responses: List) = responses + private fun extractFaceCaptures(responses: List) = responses .filterIsInstance() .flatMap { it.results } - .mapNotNull { it.sample } - .map { FaceSample(it.template, it.format) } - private fun extractFingerprintSamples(responses: List) = responses + private fun extractFingerprintCaptures(responses: List) = responses .filterIsInstance() .flatMap { it.results } - .mapNotNull { it.sample } - .map { FingerprintSample(it.fingerIdentifier, it.template, it.templateQualityScore, it.format) } private fun build( - faceCaptureBiometricsEvents: List, - fingerprintCaptureBiometricsEvents: List, - faceSamplesForPersonCreation: List?, - fingerprintSamplesForPersonCreation: List? - ) = PersonCreationEvent( - startTime = timeHelper.now(), - fingerprintCaptureIds = extractFingerprintCaptureEventIdsBasedOnPersonTemplate( - fingerprintCaptureBiometricsEvents, - fingerprintSamplesForPersonCreation?.map { encodingUtils.byteArrayToBase64(it.template) } - ), - fingerprintReferenceId = fingerprintSamplesForPersonCreation?.uniqueId(), - faceCaptureIds = extractFaceCaptureEventIdsBasedOnPersonTemplate( - faceCaptureBiometricsEvents, - faceSamplesForPersonCreation?.map { encodingUtils.byteArrayToBase64(it.template) } - ), - faceReferenceId = faceSamplesForPersonCreation?.uniqueId() - ) + faceSamplesForPersonCreation: List, + fingerprintSamplesForPersonCreation: List, + ): PersonCreationEvent { + val fingerprintCaptureIds = fingerprintSamplesForPersonCreation + .mapNotNull { it.captureEventId } + .ifEmpty { null } + val fingerprintReferenceId = fingerprintSamplesForPersonCreation + .mapNotNull { it.sample } + .map { FingerprintSample(it.fingerIdentifier, it.template, it.templateQualityScore, it.format) } + .uniqueId() - private fun extractFingerprintCaptureEventIdsBasedOnPersonTemplate( - captureEvents: List, - personTemplates: List? - ): List? = captureEvents - .filter { personTemplates?.contains(it.payload.fingerprint.template) == true } - .map { it.payload.id } - .ifEmpty { null } + val faceCaptureIds = faceSamplesForPersonCreation + .mapNotNull { it.captureEventId } + .ifEmpty { null } + val faceReferenceId = faceSamplesForPersonCreation + .mapNotNull { it.sample } + .map { FaceSample(it.template, it.format) } + .uniqueId() - private fun extractFaceCaptureEventIdsBasedOnPersonTemplate( - captureEvents: List, - personTemplates: List? - ): List? = captureEvents - .filter { personTemplates?.contains(it.payload.face.template) == true } - .map { it.payload.id } - .ifEmpty { null } + return PersonCreationEvent( + startTime = timeHelper.now(), + fingerprintCaptureIds = fingerprintCaptureIds, + fingerprintReferenceId = fingerprintReferenceId, + faceCaptureIds = faceCaptureIds, + faceReferenceId = faceReferenceId, + ) + } private fun PersonCreationEvent.hasBiometricData() = payload.fingerprintCaptureIds?.isNotEmpty() == true || payload.faceCaptureIds?.isNotEmpty() == true diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCaseTest.kt index 9e1424ea0e..2f4259b30c 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCaseTest.kt @@ -1,18 +1,14 @@ package com.simprints.feature.orchestrator.usecases import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.tools.time.TimeHelper -import com.simprints.core.tools.utils.EncodingUtils +import com.simprints.core.tools.time.Timestamp import com.simprints.face.capture.FaceCaptureResult import com.simprints.fingerprint.capture.FingerprintCaptureResult +import com.simprints.infra.events.SessionEventRepository import com.simprints.infra.events.event.domain.models.PersonCreationEvent -import com.simprints.infra.events.event.domain.models.face.FaceCaptureBiometricsEvent -import com.simprints.infra.events.event.domain.models.face.FaceCaptureEvent import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent -import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent -import com.simprints.core.domain.fingerprint.IFingerIdentifier -import com.simprints.core.tools.time.Timestamp -import com.simprints.infra.events.SessionEventRepository import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -36,9 +32,6 @@ internal class CreatePersonEventUseCaseTest { @MockK lateinit var timeHelper: TimeHelper - @MockK - lateinit var encodingUtils: EncodingUtils - private lateinit var useCase: CreatePersonEventUseCase @Before @@ -46,20 +39,17 @@ internal class CreatePersonEventUseCaseTest { MockKAnnotations.init(this, relaxed = true) every { timeHelper.now() } returns Timestamp(0L) - every { encodingUtils.byteArrayToBase64(any()) } returns TEMPLATE coEvery { eventRepository.getCurrentSessionScope() } returns mockk { every { id } returns "sessionId" } - useCase = CreatePersonEventUseCase(eventRepository, timeHelper, encodingUtils) + useCase = CreatePersonEventUseCase(eventRepository, timeHelper) } @Test fun `Does not create event if has person creation in session`() = runTest { coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - mockk(), - mockk(), mockk(), ) @@ -70,10 +60,7 @@ internal class CreatePersonEventUseCaseTest { @Test fun `Does not create event if no biometric data`() = runTest { - coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - mockk(), - mockk(), - ) + coEvery { eventRepository.getEventsInCurrentSession() } returns listOf() useCase(listOf()) @@ -82,18 +69,13 @@ internal class CreatePersonEventUseCaseTest { @Test fun `Create event if there is face biometric data`() = runTest { - coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - mockk { - every { payload.id } returns "eventFaceId1" - every { payload.face.template } returns TEMPLATE - }, - ) + coEvery { eventRepository.getEventsInCurrentSession() } returns listOf() useCase(listOf(FaceCaptureResult(listOf(createFaceCaptureResultItem())))) coVerify { eventRepository.addOrUpdateEvent(withArg { - assertThat(it.payload.faceCaptureIds).isEqualTo(listOf("eventFaceId1")) + assertThat(it.payload.faceCaptureIds).isEqualTo(listOf(FACE_ID)) }) } } @@ -102,7 +84,7 @@ internal class CreatePersonEventUseCaseTest { fun `Create event if there is fingerprint biometric data`() = runTest { coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( mockk { - every { payload.id } returns "eventFinger1" + every { payload.id } returns FINGER_ID every { payload.fingerprint.template } returns TEMPLATE }, ) @@ -111,22 +93,36 @@ internal class CreatePersonEventUseCaseTest { coVerify { eventRepository.addOrUpdateEvent(withArg { - assertThat(it.payload.fingerprintCaptureIds).isEqualTo(listOf("eventFinger1")) + assertThat(it.payload.fingerprintCaptureIds).isEqualTo(listOf(FINGER_ID)) }) } } private fun createFingerprintCaptureResultItem() = FingerprintCaptureResult.Item( - IFingerIdentifier.RIGHT_THUMB, - FingerprintCaptureResult.Sample(IFingerIdentifier.RIGHT_THUMB, byteArrayOf(), 0, null, "format") + captureEventId = FINGER_ID, + identifier = IFingerIdentifier.RIGHT_THUMB, + sample = FingerprintCaptureResult.Sample( + IFingerIdentifier.RIGHT_THUMB, + TEMPLATE.toByteArray(), + 0, + null, + "format" + ) ) + private fun createFaceCaptureResultItem() = - FaceCaptureResult.Item(0, FaceCaptureResult.Sample("faceId", byteArrayOf(), null, "format")) + FaceCaptureResult.Item( + captureEventId = FACE_ID, + index = 0, + sample = FaceCaptureResult.Sample(FACE_ID, TEMPLATE.toByteArray(), null, "format") + ) companion object { - const val TEMPLATE = "template" + private const val TEMPLATE = "template" + private const val FINGER_ID = "eventFinger1" + private const val FACE_ID = "eventFinger1" } } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt index a60f45d86f..4a4d4a92dd 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt @@ -16,6 +16,7 @@ import com.simprints.infra.events.event.domain.models.subject.FaceTemplate import com.simprints.infra.events.event.domain.models.subject.FingerprintReference import com.simprints.infra.events.event.domain.models.subject.FingerprintTemplate import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK @@ -153,6 +154,7 @@ class SubjectFactoryTest { moduleId = expected.moduleId, fingerprintResponse = FingerprintCaptureResult(listOf( FingerprintCaptureResult.Item( + captureEventId = GUID1, identifier = IDENTIFIER, sample = FingerprintCaptureResult.Sample( template = BASE_64_BYTES, @@ -165,6 +167,7 @@ class SubjectFactoryTest { )), faceResponse = FaceCaptureResult(listOf( FaceCaptureResult.Item( + captureEventId = GUID1, index = 0, sample = FaceCaptureResult.Sample( template = BASE_64_BYTES, From ace22497cfd6c5cd9b82c5fca4aac038b3ec6575 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 22 May 2024 10:19:44 +0100 Subject: [PATCH 6/8] MS-471 Matching default lowest confidence value decreased to minimum possible --- .../main/java/com/simprints/matcher/usecases/MatchResultSet.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatchResultSet.kt b/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatchResultSet.kt index 4f2f95cf70..d7e287e109 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatchResultSet.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatchResultSet.kt @@ -7,7 +7,7 @@ internal class MatchResultSet( private val maxSize: Int = MAX_RESULTS, ) { - private var lowestConfidence: Float = Float.MIN_VALUE + private var lowestConfidence: Float = 0f private val treeSet = TreeSet { o1: T, o2: T -> // Reverse order for descending sort From 29db9aa27ee7e82e2702c2628d7c019d616bf464 Mon Sep 17 00:00:00 2001 From: alexandr Date: Thu, 30 May 2024 13:48:57 +0300 Subject: [PATCH 7/8] [MS-483] Downgrading retrofit from 2.11.0 to 2.9.0 for the compatabilities with the <=25 SDKs --- gradle/libs.versions.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad46f07c87..567f5c633d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,8 @@ firebase_crashlyticsPlugin_version = "2.9.9" firebase_perfPlugin_version = "1.4.2" firebase_distrtibutionPlugin_version = "4.2.0" -retrofit_version = "2.11.0" +# [MS-483] Newer versions of Retrofit (>=2.10.0) don't support android apis <=25 beacuse of the internal Jackson library +retrofit_version = "2.9.0" okttp_version = "4.12.0" # Newer versions of Jackson don't support android apis <=25.Jackson shouldn't be updated as long as SID MIN supported APIs <=25 jackson_version = "2.13.4" From d3ef1bd20be16e23492f6c84ebd3351aab0e863c Mon Sep 17 00:00:00 2001 From: melad Date: Mon, 3 Jun 2024 08:38:24 +0300 Subject: [PATCH 8/8] Fix tests --- .../capture/screen/FingerprintCaptureViewModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt index cc78aacaa0..773a407964 100644 --- a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt +++ b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt @@ -941,7 +941,7 @@ class FingerprintCaptureViewModelTest { vm.handleConfirmFingerprintsAndContinue() // Save image is called even if scanResult.image == null - 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)