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 @@ -155,6 +155,9 @@ class BuildSubjectUseCaseTest {
documentImagePath = null,
zoomedCredentialImagePath = null,
credentialBoundingBox = null,
scanStartTime = Timestamp(1L),
scanEndTime = Timestamp(1L),
scannedValue = TokenizableString.Raw("test"),
)

val result = useCase(createParams(steps = emptyList(), scannedCredential = scannedCredential), isAddingCredential = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,21 @@ import com.simprints.core.domain.externalcredential.ExternalCredentialType
import com.simprints.core.livedata.LiveDataEventWithContent
import com.simprints.core.livedata.send
import com.simprints.core.tools.time.TimeHelper
import com.simprints.core.tools.time.Timestamp
import com.simprints.feature.externalcredential.ExternalCredentialSearchResult
import com.simprints.feature.externalcredential.model.ExternalCredentialParams
import com.simprints.feature.externalcredential.screens.search.model.toExternalCredential
import com.simprints.feature.externalcredential.usecase.ExternalCredentialEventTrackerUseCase
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureEvent
import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureValueEvent
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.infra.logging.Simber
import com.simprints.infra.events.event.domain.models.ExternalCredentialSelectionEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject
import kotlin.collections.orEmpty

@HiltViewModel
internal class ExternalCredentialViewModel @Inject internal constructor(
private val timeHelper: TimeHelper,
private val configManager: ConfigManager,
private val eventRepository: SessionEventRepository,
private val eventsTracker: ExternalCredentialEventTrackerUseCase,
) : ViewModel() {
private var isInitialized = false
lateinit var params: ExternalCredentialParams
Expand All @@ -46,6 +42,12 @@ internal class ExternalCredentialViewModel @Inject internal constructor(
get() = _externalCredentialTypes
private val _externalCredentialTypes = MutableLiveData<List<ExternalCredentialType>>()

private lateinit var selectionStartTime: Timestamp
private lateinit var selectionEventId: String
private lateinit var captureStartTime: Timestamp
private var selectedSkipReason: ExternalCredentialSelectionEvent.SkipReason? = null
private var selectedSkipOtherText: String? = null

init {
viewModelScope.launch {
val config = configManager.getProjectConfiguration()
Expand All @@ -54,12 +56,31 @@ internal class ExternalCredentialViewModel @Inject internal constructor(
}
}

fun selectionStarted() {
selectionStartTime = timeHelper.now()
}

fun skipOptionSelected(skipOption: ExternalCredentialSelectionEvent.SkipReason) {
selectedSkipReason = skipOption
}

fun skipOtherReasonChanged(otherText: String?) {
selectedSkipOtherText = otherText?.ifBlank { null }
}

private fun updateState(state: (ExternalCredentialState) -> ExternalCredentialState) {
this.state = state(this.state)
}

fun setSelectedExternalCredentialType(selectedType: ExternalCredentialType?) {
updateState { it.copy(selectedType = selectedType) }
viewModelScope.launch {
if (selectedType != null) {
val selectionEndTime = timeHelper.now()
selectionEventId = eventsTracker.saveSelectionEvent(selectionStartTime, selectionEndTime, selectedType)
captureStartTime = timeHelper.now()
}
updateState { it.copy(selectedType = selectedType) }
}
}

fun setExternalCredentialValue(value: String) {
Expand All @@ -76,28 +97,16 @@ internal class ExternalCredentialViewModel @Inject internal constructor(

fun finish(result: ExternalCredentialSearchResult) {
viewModelScope.launch {
result.scannedCredential?.let { scannedCredential ->
Simber.d("Saving External Credential Events for $scannedCredential")
val credential = scannedCredential.toExternalCredential(params.subjectId.orEmpty())
eventRepository.addOrUpdateEvent(
ExternalCredentialCaptureValueEvent(
createdAt = timeHelper.now(),
id = UUID.randomUUID().toString(),
credential = credential,
),
)
eventRepository.addOrUpdateEvent(
ExternalCredentialCaptureEvent(
createdAt = timeHelper.now(),
id = UUID.randomUUID().toString(),
endTime = timeHelper.now(),
autoCaptureStartTime = timeHelper.now(),
autoCaptureEndTime = timeHelper.now(),
ocrErrorCount = 0,
capturedTextLength = 0,
credentialTextLength = 0,
selectionId = credential.id,
),
if (result.scannedCredential == null) {
selectedSkipReason?.let { reason ->
eventsTracker.saveSkippedEvent(selectionStartTime, reason, selectedSkipOtherText)
}
} else {
eventsTracker.saveCaptureEvents(
captureStartTime,
params.subjectId.orEmpty(),
result.scannedCredential,
selectionEventId,
)
}
_finishEvent.send(result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.core.domain.tokenization.asTokenizableRaw
import com.simprints.core.livedata.LiveDataEventWithContent
import com.simprints.core.livedata.send
import com.simprints.core.tools.time.TimeHelper
import com.simprints.core.tools.time.Timestamp
import com.simprints.feature.externalcredential.screens.scanocr.model.DetectedOcrBlock
import com.simprints.feature.externalcredential.screens.scanocr.model.OcrCropConfig
import com.simprints.feature.externalcredential.screens.scanocr.model.OcrDocumentType
Expand Down Expand Up @@ -37,6 +39,7 @@ import kotlinx.coroutines.launch

internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor(
@Assisted val ocrDocumentType: OcrDocumentType,
private val timeHelper: TimeHelper,
private val normalizeBitmapToPreviewUseCase: NormalizeBitmapToPreviewUseCase,
private val cropDocumentFromPreviewUseCase: CropDocumentFromPreviewUseCase,
private val getCredentialCoordinatesUseCase: GetCredentialCoordinatesUseCase,
Expand Down Expand Up @@ -69,6 +72,8 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor(
get() = _finishOcrEvent
private val _finishOcrEvent = MutableLiveData<LiveDataEventWithContent<ScannedCredential>>()

private lateinit var startTime: Timestamp

private fun updateState(state: (ScanOcrState) -> ScanOcrState) {
this.state = state(this.state)
}
Expand All @@ -79,6 +84,7 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor(
}

fun ocrStarted() {
startTime = timeHelper.now()
Comment thread
alexandr-simprints marked this conversation as resolved.
updateState {
ScanOcrState.ScanningInProgress(
ocrDocumentType = ocrDocumentType,
Expand Down Expand Up @@ -121,17 +127,22 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor(
val credentialType = detectedBlock.documentType.asExternalCredentialType()
val blockBoundingBox = detectedBlock.blockBoundingBox
val zoomedCredentialImagePath = buildZoomedImagePath(detectedBlock)
val detectedValueRaw = detectedBlock.readoutValue.asTokenizableRaw()
val credential = tokenizationProcessor.encrypt(
decrypted = detectedBlock.readoutValue.asTokenizableRaw(),
decrypted = detectedValueRaw,
tokenKeyType = TokenKeyType.ExternalCredential,
project = project,
) as TokenizableString.Tokenized

val scannedCredential = ScannedCredential(
credential = credential,
credentialType = credentialType,
documentImagePath = detectedBlock.imagePath,
zoomedCredentialImagePath = zoomedCredentialImagePath,
credentialBoundingBox = blockBoundingBox,
scanStartTime = startTime,
scanEndTime = timeHelper.now(),
scannedValue = detectedValueRaw,
)
_finishOcrEvent.send(scannedCredential)
detectedBlocks = emptyList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.getValue
import com.simprints.infra.resources.R as IDR

@AndroidEntryPoint
Expand Down Expand Up @@ -144,6 +143,9 @@ internal class ExternalCredentialScanQrFragment : Fragment(R.layout.fragment_ext
documentImagePath = null,
credentialBoundingBox = null,
zoomedCredentialImagePath = null,
scanStartTime = state.scanStartTime,
scanEndTime = state.scanEndTime,
scannedValue = state.qrCode,
)
findNavController().navigateSafely(
this@ExternalCredentialScanQrFragment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.lifecycle.viewModelScope
import com.simprints.core.domain.permission.PermissionStatus
import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.core.domain.tokenization.asTokenizableRaw
import com.simprints.core.tools.time.TimeHelper
import com.simprints.core.tools.time.Timestamp
import com.simprints.feature.externalcredential.screens.scanqr.usecase.ExternalCredentialQrCodeValidatorUseCase
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.store.models.TokenKeyType
Expand All @@ -18,11 +20,13 @@ import javax.inject.Inject

@HiltViewModel
internal class ExternalCredentialScanQrViewModel @Inject constructor(
private val timeHelper: TimeHelper,
private val externalCredentialQrCodeValidator: ExternalCredentialQrCodeValidatorUseCase,
private val authStore: AuthStore,
private val configManager: ConfigManager,
private val tokenizationProcessor: TokenizationProcessor,
) : ViewModel() {
private lateinit var startTime: Timestamp
private var state: ScanQrState = ScanQrState.ReadyToScan
set(value) {
field = value
Expand All @@ -46,7 +50,12 @@ internal class ExternalCredentialScanQrViewModel @Inject constructor(
tokenKeyType = TokenKeyType.ExternalCredential,
project = project,
) as TokenizableString.Tokenized
ScanQrState.QrCodeCaptured(qrCode = value.asTokenizableRaw(), qrCodeEncrypted = qrCodeEncrypted)
ScanQrState.QrCodeCaptured(
scanStartTime = startTime,
scanEndTime = timeHelper.now(),
qrCode = value.asTokenizableRaw(),
qrCodeEncrypted = qrCodeEncrypted,
)
}
}
updateState { newState }
Expand All @@ -55,7 +64,10 @@ internal class ExternalCredentialScanQrViewModel @Inject constructor(

fun updateCameraPermissionStatus(permissionStatus: PermissionStatus) {
val newState = when (permissionStatus) {
PermissionStatus.Granted -> ScanQrState.ReadyToScan
PermissionStatus.Granted -> {
startTime = timeHelper.now() // Reset scan timer
ScanQrState.ReadyToScan
}
PermissionStatus.Denied -> ScanQrState.NoCameraPermission(shouldOpenPhoneSettings = false)
PermissionStatus.DeniedNeverAskAgain -> ScanQrState.NoCameraPermission(shouldOpenPhoneSettings = true)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.simprints.feature.externalcredential.screens.scanqr

import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.core.tools.time.Timestamp

sealed class ScanQrState {
data object ReadyToScan : ScanQrState()
Expand All @@ -10,6 +11,8 @@ sealed class ScanQrState {
) : ScanQrState()

data class QrCodeCaptured(
val scanStartTime: Timestamp,
val scanEndTime: Timestamp,
val qrCode: TokenizableString.Raw,
val qrCodeEncrypted: TokenizableString.Tokenized,
) : ScanQrState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_ext
viewModel.finish(state)
}
buttonRecapture.setOnClickListener {
viewModel.trackRecapture()
findNavController().navigateSafely(
this@ExternalCredentialSearchFragment,
ExternalCredentialSearchFragmentDirections.actionExternalCredentialSearchToExternalCredentialSelectFragment(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ import com.simprints.core.domain.externalcredential.ExternalCredentialType
import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.core.livedata.LiveDataEventWithContent
import com.simprints.core.livedata.send
import com.simprints.core.tools.time.TimeHelper
import com.simprints.feature.externalcredential.ExternalCredentialSearchResult
import com.simprints.feature.externalcredential.model.ExternalCredentialParams
import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential
import com.simprints.feature.externalcredential.screens.search.model.SearchCredentialState
import com.simprints.feature.externalcredential.screens.search.model.SearchState
import com.simprints.feature.externalcredential.screens.search.usecase.MatchCandidatesUseCase
import com.simprints.feature.externalcredential.usecase.ExternalCredentialEventTrackerUseCase
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.store.models.TokenKeyType
import com.simprints.infra.config.store.tokenization.TokenizationProcessor
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository
import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery
import com.simprints.infra.events.event.domain.models.ExternalCredentialConfirmationEvent.ExternalCredentialConfirmationResult
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
Expand All @@ -31,11 +34,13 @@ import com.simprints.infra.resources.R as IDR
internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
@Assisted val scannedCredential: ScannedCredential,
@Assisted val externalCredentialParams: ExternalCredentialParams,
private val timeHelper: TimeHelper,
private val authStore: AuthStore,
private val configManager: ConfigManager,
private val matchCandidatesUseCase: MatchCandidatesUseCase,
private val tokenizationProcessor: TokenizationProcessor,
private val enrolmentRecordRepository: EnrolmentRecordRepository,
private val eventsTracker: ExternalCredentialEventTrackerUseCase,
) : ViewModel() {
@AssistedFactory
interface Factory {
Expand All @@ -57,6 +62,8 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
private val _stateLiveData = MutableLiveData(state)
val stateLiveData: LiveData<SearchCredentialState> = _stateLiveData

private val confirmationStartTime = timeHelper.now()

private fun updateState(state: (SearchCredentialState) -> SearchCredentialState) {
this.state = state(this.state)
}
Expand Down Expand Up @@ -129,11 +136,19 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
updateState { it.copy(searchState = SearchState.Searching) }
val project = configManager.getProject(authStore.signedInProjectId)
val candidates = enrolmentRecordRepository.load(SubjectQuery(projectId = project.id, externalCredential = credential))

val startTime = timeHelper.now()
when {
candidates.isEmpty() -> updateState { it.copy(searchState = SearchState.CredentialNotFound) }
candidates.isEmpty() -> {
eventsTracker.saveSearchEvent(startTime, scannedCredential.credentialScanId, emptyList())
Comment thread
alexandr-simprints marked this conversation as resolved.
updateState { it.copy(searchState = SearchState.CredentialNotFound) }
}

else -> {
val projectConfig = configManager.getProjectConfiguration()
val matches = matchCandidatesUseCase(candidates, credential, externalCredentialParams, project, projectConfig)
eventsTracker.saveSearchEvent(startTime, scannedCredential.credentialScanId, candidates)

updateState { state -> state.copy(searchState = SearchState.CredentialLinked(matchResults = matches)) }
}
}
Expand All @@ -149,20 +164,35 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
ExternalCredentialType.QRCode -> InputType.TYPE_CLASS_TEXT
}

fun trackRecapture() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luhmirin-s shouldn't you reset the val startTime here ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recapture navigates to the selection fragment that will reset the respective timestamps in onViewCreated() and subsequent methods.

viewModelScope.launch {
eventsTracker.saveConfirmation(
confirmationStartTime,
ExternalCredentialConfirmationResult.RECAPTURE,
)
}
}

fun finish(state: SearchCredentialState) {
val matches = when (val searchState = state.searchState) {
SearchState.Searching,
SearchState.CredentialNotFound,
-> emptyList()
viewModelScope.launch {
Comment thread
luhmirin-s marked this conversation as resolved.
eventsTracker.saveConfirmation(
confirmationStartTime,
ExternalCredentialConfirmationResult.CONTINUE,
)
val matches = when (val searchState = state.searchState) {
SearchState.Searching,
SearchState.CredentialNotFound,
-> emptyList()

is SearchState.CredentialLinked -> searchState.matchResults
is SearchState.CredentialLinked -> searchState.matchResults
}
_finishEvent.send(
ExternalCredentialSearchResult(
flowType = externalCredentialParams.flowType,
scannedCredential = state.scannedCredential,
matchResults = matches,
),
)
}
_finishEvent.send(
ExternalCredentialSearchResult(
flowType = externalCredentialParams.flowType,
scannedCredential = state.scannedCredential,
matchResults = matches,
),
)
}
}
Loading