diff --git a/build-logic/build_properties.gradle.kts b/build-logic/build_properties.gradle.kts index 1f50152ac9..e65867a496 100644 --- a/build-logic/build_properties.gradle.kts +++ b/build-logic/build_properties.gradle.kts @@ -19,7 +19,7 @@ extra.apply { * Dev version >= 2025.2.0 is required to support enrolment record updates and SimFace configuration * Dev version >= 2025.3.0 is required to receive smaples and structured down sync configuration */ - set("VERSION_NAME", "2025.3.0") + set("VERSION_NAME", "2025.4.0") /** * Build type. The version code describes which build type was used for the build. 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 23cd5993dc..d12839ee2c 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 @@ -1,6 +1,7 @@ package com.simprints.face.capture import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.step.StepResult import com.simprints.infra.images.model.SecuredImageRef @@ -22,5 +23,29 @@ data class FaceCaptureResult( val template: ByteArray, val imageRef: SecuredImageRef?, val format: String, - ) : StepResult + ) : StepResult { + @ExcludedFromGeneratedTestCoverageReports("Generated code") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Sample + + if (faceId != other.faceId) return false + if (!template.contentEquals(other.template)) return false + if (imageRef != other.imageRef) return false + if (format != other.format) return false + + return true + } + + @ExcludedFromGeneratedTestCoverageReports("Generated code") + override fun hashCode(): Int { + var result = faceId.hashCode() + result = 31 * result + template.contentHashCode() + result = 31 * result + (imageRef?.hashCode() ?: 0) + result = 31 * result + format.hashCode() + return result + } + } } diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoFragment.kt index f5285274a2..f54e60d795 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoFragment.kt @@ -354,12 +354,14 @@ internal class SyncInfoFragment : Fragment(R.layout.fragment_sync_info) { moduleCountAdapter.submitList(moduleCountsForAdapter) // RecyclerView height fix (wrong height may be caused by ConstraintLayout in parent views) - binding.selectedModulesView.post { - val itemHeight = resources.getDimensionPixelSize(R.dimen.module_item_height) - val itemCount = moduleCountsForAdapter.size.coerceAtMost(MAX_MODULE_LIST_HEIGHT_ITEMS) - binding.selectedModulesView.apply { - layoutParams = layoutParams.apply { - height = itemHeight * itemCount + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + val itemHeight = resources.getDimensionPixelSize(R.dimen.module_item_height) + val itemCount = moduleCountsForAdapter.size.coerceAtMost(MAX_MODULE_LIST_HEIGHT_ITEMS) + binding.selectedModulesView.apply { + layoutParams = layoutParams.apply { + height = itemHeight * itemCount + } } } } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/CredentialMatch.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/CredentialMatch.kt index a628729173..3fd619dac9 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/CredentialMatch.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/CredentialMatch.kt @@ -2,6 +2,7 @@ package com.simprints.feature.externalcredential.model import androidx.annotation.Keep import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.step.StepResult import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration @@ -15,6 +16,6 @@ data class CredentialMatch( val verificationThreshold: Float, val faceBioSdk: FaceConfiguration.BioSdk?, val fingerprintBioSdk: FingerprintConfiguration.BioSdk?, -) { +) : StepResult { val isVerificationSuccessful = matchResult.confidence >= verificationThreshold } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/ScannedCredential.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/ScannedCredential.kt index 134340f857..bf2c269b61 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/ScannedCredential.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/ScannedCredential.kt @@ -3,6 +3,7 @@ package com.simprints.feature.externalcredential.screens.search.model import androidx.annotation.Keep import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.core.domain.step.StepResult import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.tools.time.Timestamp import com.simprints.core.tools.utils.randomUUID @@ -20,7 +21,7 @@ data class ScannedCredential( val scanStartTime: Timestamp, val scanEndTime: Timestamp, val scannedValue: TokenizableString.Raw, -) : Serializable +) : StepResult fun ScannedCredential.toExternalCredential(subjectId: String) = ExternalCredential( id = credentialScanId, diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt index 8d0199f78e..2360d0b4e3 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt @@ -126,6 +126,14 @@ abstract class StepResultMixin : StepResult ) abstract class StepParamsMixin : StepParams +/** + * Step contains all of the information required to execute an orchestration step and the result of the execution. + * + * All classes used in the params structure must implement [StepParams] interface and added to [StepParamsMixin]. + * All classes used in the result structure must implement [StepResult] interface and added to [StepResultMixin]. + * + * Additionally, [StepParams] and [StepResult] subclasses can only have fields of primitives, enums and serializeables classes. + */ @Keep internal data class Step( val id: Int, 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 ab32d72657..e450f188d0 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 @@ -1,23 +1,66 @@ package com.simprints.feature.orchestrator.cache import android.content.SharedPreferences -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat +import androidx.test.ext.junit.runners.* +import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.FlowType +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.core.domain.response.AppErrorReason +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.json.JsonHelper +import com.simprints.core.tools.time.Timestamp import com.simprints.face.capture.FaceCaptureParams +import com.simprints.face.capture.FaceCaptureResult +import com.simprints.feature.alert.AlertResult +import com.simprints.feature.consent.ConsentParams +import com.simprints.feature.consent.ConsentResult +import com.simprints.feature.consent.ConsentType +import com.simprints.feature.enrollast.EnrolLastBiometricParams +import com.simprints.feature.enrollast.EnrolLastBiometricStepResult +import com.simprints.feature.enrollast.FaceTemplateCaptureResult +import com.simprints.feature.enrollast.FingerTemplateCaptureResult +import com.simprints.feature.enrollast.MatchResult +import com.simprints.feature.exitform.ExitFormOption +import com.simprints.feature.exitform.ExitFormResult +import com.simprints.feature.externalcredential.ExternalCredentialSearchResult +import com.simprints.feature.externalcredential.model.BoundingBox +import com.simprints.feature.externalcredential.model.CredentialMatch +import com.simprints.feature.externalcredential.model.ExternalCredentialParams +import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential +import com.simprints.feature.fetchsubject.FetchSubjectParams +import com.simprints.feature.fetchsubject.FetchSubjectResult +import com.simprints.feature.login.LoginError +import com.simprints.feature.login.LoginParams +import com.simprints.feature.login.LoginResult import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepId import com.simprints.feature.orchestrator.steps.StepStatus +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult +import com.simprints.feature.selectsubject.SelectSubjectParams +import com.simprints.feature.selectsubject.SelectSubjectResult +import com.simprints.feature.setup.SetupResult +import com.simprints.feature.validatepool.ValidateSubjectPoolFragmentParams +import com.simprints.feature.validatepool.ValidateSubjectPoolResult +import com.simprints.fingerprint.capture.FingerprintCaptureParams import com.simprints.fingerprint.capture.FingerprintCaptureResult +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration +import com.simprints.infra.config.store.models.Finger +import com.simprints.infra.config.store.models.FingerprintConfiguration +import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource +import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 +import com.simprints.infra.images.model.Path +import com.simprints.infra.images.model.SecuredImageRef +import com.simprints.infra.matching.FaceMatchResult +import com.simprints.infra.matching.FingerprintMatchResult +import com.simprints.infra.matching.MatchParams import com.simprints.infra.security.SecurityManager -import io.mockk.MockKAnnotations -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.justRun -import io.mockk.slot import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -52,12 +95,168 @@ class OrchestratorCacheIntegrationTest { } @Test - fun `Stores and restores steps`() { + fun `Stores and restores common steps`() { + val expected = listOf( + Step( + id = StepId.SETUP, + navigationActionId = 5, + destinationId = 6, + params = null, + status = StepStatus.IN_PROGRESS, + result = SetupResult(true), + ), + Step( + id = StepId.FETCH_GUID, + navigationActionId = 5, + destinationId = 6, + params = FetchSubjectParams("projectId", "subjectId", ""), + status = StepStatus.COMPLETED, + result = FetchSubjectResult(false), + ), + Step( + id = StepId.CONSENT, + navigationActionId = 5, + destinationId = 6, + params = ConsentParams(type = ConsentType.ENROL), + status = StepStatus.COMPLETED, + result = ConsentResult(true), + ), + Step( + id = StepId.ENROL_LAST_BIOMETRIC, + navigationActionId = 5, + destinationId = 6, + params = EnrolLastBiometricParams( + projectId = "projectId", + userId = TokenizableString.Raw("value"), + moduleId = TokenizableString.Raw("value"), + steps = listOf( + EnrolLastBiometricStepResult.FingerprintCaptureResult( + "referenceId", + listOf( + FingerTemplateCaptureResult( + Finger.LEFT_4TH_FINGER, + byteArrayOf(1, 2, 3), + 10, + "NEC", + ), + ), + ), + EnrolLastBiometricStepResult.FingerprintMatchResult( + listOf(MatchResult("subjectId", 0.5f)), + FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, + ), + EnrolLastBiometricStepResult.FaceCaptureResult( + "referenceId", + listOf( + FaceTemplateCaptureResult(byteArrayOf(1, 2, 3), "RankOne"), + ), + ), + EnrolLastBiometricStepResult.FaceMatchResult( + listOf(MatchResult("subjectId", 0.5f)), + FaceConfiguration.BioSdk.RANK_ONE, + ), + EnrolLastBiometricStepResult.EnrolLastBiometricsResult("subjectId"), + ), + scannedCredential = null, + ), + status = StepStatus.COMPLETED, + result = ValidateSubjectPoolResult(true), + ), + Step( + id = StepId.CONFIRM_IDENTITY, + navigationActionId = 5, + destinationId = 6, + params = SelectSubjectParams("projectId", "subjectId", null), + status = StepStatus.COMPLETED, + result = SelectSubjectResult(true, savedCredential = null), + ), + Step( + id = StepId.VALIDATE_ID_POOL, + navigationActionId = 5, + destinationId = 6, + params = ValidateSubjectPoolFragmentParams(SubjectQuery()), + status = StepStatus.COMPLETED, + result = ValidateSubjectPoolResult(true), + ), + Step( + id = StepId.SELECT_SUBJECT_AGE, + navigationActionId = 5, + destinationId = 6, + params = null, + status = StepStatus.COMPLETED, + result = SelectSubjectAgeGroupResult(AgeGroup(10, null)), + ), + Step( + id = StepId.EXTERNAL_CREDENTIAL, + navigationActionId = 5, + destinationId = 6, + params = ExternalCredentialParams( + subjectId = "subjectId", + flowType = FlowType.IDENTIFY, + ageGroup = AgeGroup(1, 2), + probeReferenceId = "referenceId", + faceSamples = listOf( + MatchParams.FaceSample( + faceId = "faceId", + template = byteArrayOf(1, 2, 3), + ), + ), + fingerprintSamples = listOf( + MatchParams.FingerprintSample( + fingerId = IFingerIdentifier.LEFT_4TH_FINGER, + format = "NEC", + template = byteArrayOf(1, 2, 3), + ), + ), + ), + status = StepStatus.COMPLETED, + result = ExternalCredentialSearchResult( + flowType = FlowType.IDENTIFY, + scannedCredential = ScannedCredential( + credentialScanId = "scanId", + credential = "credential".asTokenizableEncrypted(), + credentialType = ExternalCredentialType.GhanaIdCard, + documentImagePath = "image/path.jpg", + zoomedCredentialImagePath = "image/path.jpg", + credentialBoundingBox = BoundingBox(0, 1, 2, 3), + scanStartTime = Timestamp(1L), + scanEndTime = Timestamp(2L, false, 123L), + scannedValue = "credential".asTokenizableRaw(), + ), + matchResults = listOf( + CredentialMatch( + credential = "credential".asTokenizableEncrypted(), + matchResult = FaceMatchResult.Item("subjectId", 0.5f), + verificationThreshold = 55f, + faceBioSdk = FaceConfiguration.BioSdk.RANK_ONE, + fingerprintBioSdk = FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, + ), + ), + ), + ), + ) + + cache.steps = expected + val actual = cache.steps + + assertThat(actual).hasSize(expected.size) + for (i in expected.indices) { + compareStubs(expected[i], actual[i]) + } + } + + @Test + fun `Stores and restores fingerprint modality steps`() { val expected = listOf( Step( id = StepId.FINGERPRINT_CAPTURE, navigationActionId = 3, destinationId = 4, + params = FingerprintCaptureParams( + flowType = FlowType.ENROL, + fingerprintsToCapture = listOf(IFingerIdentifier.LEFT_4TH_FINGER), + fingerprintSDK = FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, + ), status = StepStatus.COMPLETED, result = FingerprintCaptureResult( "", @@ -65,27 +264,143 @@ class OrchestratorCacheIntegrationTest { FingerprintCaptureResult.Item( captureEventId = GUID1, identifier = IFingerIdentifier.LEFT_THUMB, - sample = null, + sample = FingerprintCaptureResult.Sample( + fingerIdentifier = IFingerIdentifier.LEFT_4TH_FINGER, + template = byteArrayOf(1, 2, 3), + templateQualityScore = 10, + imageRef = SecuredImageRef(Path("file/path")), + format = "NEC", + ), ), ), ), ), + Step( + id = StepId.FINGERPRINT_MATCHER, + navigationActionId = 3, + destinationId = 4, + params = MatchParams( + probeReferenceId = GUID1, + flowType = FlowType.IDENTIFY, + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.CommCare("name"), + probeFingerprintSamples = listOf( + MatchParams.FingerprintSample( + fingerId = IFingerIdentifier.LEFT_4TH_FINGER, + format = "NEC", + template = byteArrayOf(1, 2, 3), + ), + ), + ), + status = StepStatus.COMPLETED, + result = FingerprintMatchResult( + listOf(FingerprintMatchResult.Item("subjectId", 0.5f)), + FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, + ), + ), + ) + + cache.steps = expected + val actual = cache.steps + + assertThat(actual).hasSize(expected.size) + for (i in expected.indices) { + compareStubs(expected[i], actual[i]) + } + } + + @Test + fun `Stores and restores face modality steps`() { + val expected = listOf( Step( id = StepId.FACE_CAPTURE, navigationActionId = 5, destinationId = 6, params = FaceCaptureParams(3, FaceConfiguration.BioSdk.RANK_ONE), status = StepStatus.COMPLETED, - result = null, + result = FaceCaptureResult( + "", + results = listOf( + FaceCaptureResult.Item( + captureEventId = "event", + index = 0, + sample = FaceCaptureResult.Sample( + faceId = "faceId", + template = byteArrayOf(1, 2, 3), + imageRef = SecuredImageRef(Path("file/path")), + format = "ROC", + ), + ), + ), + ), + ), + Step( + id = StepId.FACE_MATCHER, + navigationActionId = 3, + destinationId = 4, + params = MatchParams( + probeReferenceId = GUID1, + flowType = FlowType.IDENTIFY, + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.Simprints, + probeFaceSamples = listOf( + MatchParams.FaceSample( + faceId = "faceId", + template = byteArrayOf(1, 2, 3), + ), + ), + ), + status = StepStatus.COMPLETED, + result = FaceMatchResult( + listOf(FaceMatchResult.Item("subjectId", 0.5f)), + FaceConfiguration.BioSdk.RANK_ONE, + ), + ), + ) + + cache.steps = expected + val actual = cache.steps + + assertThat(actual).hasSize(expected.size) + for (i in expected.indices) { + compareStubs(expected[i], actual[i]) + } + } + + @Test + fun `Stores and restores exception steps`() { + val expected = listOf( + Step( + id = 1, + navigationActionId = 5, + destinationId = 6, + params = LoginParams("projectId", TokenizableString.Tokenized("value")), + status = StepStatus.NOT_STARTED, + result = LoginResult(false, LoginError.LoginNotCompleted), + ), + Step( + id = 2, + navigationActionId = 5, + destinationId = 6, + status = StepStatus.NOT_STARTED, + result = AlertResult("key", AppErrorReason.UNEXPECTED_ERROR), + ), + Step( + id = 3, + navigationActionId = 5, + destinationId = 6, + status = StepStatus.NOT_STARTED, + result = ExitFormResult(true, ExitFormOption.DataConcerns), ), ) cache.steps = expected val actual = cache.steps - assertThat(actual).hasSize(2) - compareStubs(expected[0], actual[0]) - compareStubs(expected[1], actual[1]) + assertThat(actual).hasSize(expected.size) + for (i in expected.indices) { + compareStubs(expected[i], actual[i]) + } } private fun compareStubs( 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 ad73c56792..a8c8b916ef 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 @@ -1,6 +1,7 @@ package com.simprints.fingerprint.capture import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.domain.step.StepResult import com.simprints.infra.images.model.SecuredImageRef @@ -24,5 +25,31 @@ data class FingerprintCaptureResult( val templateQualityScore: Int, val imageRef: SecuredImageRef?, val format: String, - ) : StepResult + ) : StepResult { + @ExcludedFromGeneratedTestCoverageReports("Generated code") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Sample + + if (templateQualityScore != other.templateQualityScore) return false + if (fingerIdentifier != other.fingerIdentifier) return false + if (!template.contentEquals(other.template)) return false + if (imageRef != other.imageRef) return false + if (format != other.format) return false + + return true + } + + @ExcludedFromGeneratedTestCoverageReports("Generated code") + override fun hashCode(): Int { + var result = templateQualityScore + result = 31 * result + fingerIdentifier.hashCode() + result = 31 * result + template.contentHashCode() + result = 31 * result + (imageRef?.hashCode() ?: 0) + result = 31 * result + format.hashCode() + return result + } + } } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/BiometricDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/BiometricDataSource.kt index d468c0e756..6b69d6abb3 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/BiometricDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/BiometricDataSource.kt @@ -13,7 +13,7 @@ sealed class BiometricDataSource : StepParams { data object Simprints : BiometricDataSource() data class CommCare( - private val callerPackageName: String, + val callerPackageName: String, ) : BiometricDataSource() { override fun callerPackageName() = callerPackageName diff --git a/infra/images/src/main/java/com/simprints/infra/images/remote/firebase/FirebaseSampleUploader.kt b/infra/images/src/main/java/com/simprints/infra/images/remote/firebase/FirebaseSampleUploader.kt index 9017636d1b..dd7c17b7ac 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/remote/firebase/FirebaseSampleUploader.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/remote/firebase/FirebaseSampleUploader.kt @@ -17,7 +17,6 @@ import com.simprints.infra.images.model.SecuredImageRef import com.simprints.infra.images.remote.SampleUploader import com.simprints.infra.images.usecase.SamplePathConverter import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SAMPLE_UPLOAD -import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SYNC import com.simprints.infra.logging.Simber import kotlinx.coroutines.tasks.await import java.io.FileInputStream @@ -103,7 +102,7 @@ internal class FirebaseSampleUploader @Inject constructor( val fileRef = imageRef.relativePath.parts .fold(rootRef) { ref, pathPart -> ref.child(pathPart) } - Simber.i("Uploading ${fileRef.path.last()}", tag = SAMPLE_UPLOAD) + Simber.i("Uploading ${imageRef.relativePath.parts.last()}", tag = SAMPLE_UPLOAD) return if (metadata.isEmpty()) { fileRef.putStream(imageStream).await()