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 4797939f2b..2b25f11709 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 @@ -53,7 +53,6 @@ internal class FaceCaptureViewModel @Inject constructor( private val licenseRepository: LicenseRepository, private val resolveFaceBioSdk: ResolveFaceBioSdkUseCase, private val saveLicenseCheckEvent: SaveLicenseCheckEventUseCase, - private val isUsingAutoCapture: IsUsingAutoCaptureUseCase, private val shouldShowInstructions: ShouldShowInstructionsScreenUseCase, @DeviceID private val deviceID: String, ) : ViewModel() { @@ -87,10 +86,6 @@ internal class FaceCaptureViewModel @Inject constructor( get() = _invalidLicense private val _invalidLicense = MutableLiveData() - val isAutoCaptureEnabled: LiveData - get() = _isAutoCaptureEnabled - private val _isAutoCaptureEnabled = MutableLiveData() - fun setupCapture(samplesToCapture: Int) { this.samplesToCapture = samplesToCapture } @@ -142,10 +137,6 @@ internal class FaceCaptureViewModel @Inject constructor( } } - fun setupAutoCapture() = viewModelScope.launch { - _isAutoCaptureEnabled.postValue(isUsingAutoCapture()) - } - fun shouldShowInstructionsScreen(): Boolean = shouldShowInstructions() private suspend fun initialize( diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt index feb2e60d61..53f0895e8b 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt @@ -118,26 +118,18 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c } } - viewModel.setupAutoCapture() - viewModel.isAutoCaptureEnabled.observe(viewLifecycleOwner) { isAutoCaptureEnabled -> - val graph = internalNavController?.navInflater?.inflate( - if (isAutoCaptureEnabled) { - R.navigation.graph_face_capture_auto_internal - } else { - R.navigation.graph_face_capture_internal - }, - ) - graph?.setStartDestination( - if (viewModel.shouldShowInstructionsScreen()) { - R.id.facePreparationFragment - } else { - R.id.faceLiveFeedbackFragment - }, - ) - graph?.let { - internalNavController?.setGraph(graph, null) - } - } + internalNavController + ?.navInflater + ?.inflate(R.navigation.graph_face_capture_internal) + ?.also { + it.setStartDestination( + if (viewModel.shouldShowInstructionsScreen()) { + R.id.facePreparationFragment + } else { + R.id.faceLiveFeedbackFragment + }, + ) + }?.let { internalNavController?.setGraph(it, null) } } private fun initFaceBioSdk() { diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt index cb55888b8d..ef88fb3217 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt @@ -3,7 +3,6 @@ package com.simprints.face.capture.screens.livefeedback import android.Manifest import android.content.Intent import android.graphics.Bitmap -import android.net.Uri import android.os.Bundle import android.provider.Settings import android.util.Size @@ -17,6 +16,7 @@ import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.awaitInstance import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -66,6 +66,9 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) private var cameraControl: CameraControl? = null + private val validCaptureProgressColor = ContextCompat.getColor(requireContext(), IDR.color.simprints_green_light) + private val defaultCaptureProgressColor = ContextCompat.getColor(requireContext(), IDR.color.simprints_blue_grey_light) + private val launchPermissionRequest = registerForActivityResult( ActivityResultContracts.RequestPermission(), ) { granted -> @@ -90,9 +93,14 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) private fun initFragment() { screenSize = with(resources.displayMetrics) { Size(widthPixels, widthPixels) } bindViewModel() + binding.captureProgress.max = 1 // normalized progress - binding.captureFeedbackBtn.setOnClickListener { vm.startCapture() } - binding.captureProgress.max = mainVm.samplesToCapture + binding.captureFeedbackBtn.setOnClickListener { + vm.startCapture() + if (vm.isAutoCapture) { + binding.captureFeedbackBtn.isClickable = false + } + } // Wait till the views gets its final size then init frame processor and setup the camera binding.faceCaptureCamera.post { @@ -100,6 +108,11 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) vm.initCapture(mainVm.bioSDK, mainVm.samplesToCapture, mainVm.attemptNumber) } } + if (vm.isAutoCapture) { + // Await until capture button is pressed + vm.holdOffAutoCapture() + binding.captureFeedbackBtn.isClickable = true + } binding.captureInstructionsBtn.setOnClickListener { findNavController().navigateSafely( @@ -199,11 +212,9 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) } vm.capturingState.observe(viewLifecycleOwner) { - @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") when (it) { - LiveFeedbackFragmentViewModel.CapturingState.NOT_STARTED -> renderCapturingNotStarted() - + when (it) { + LiveFeedbackFragmentViewModel.CapturingState.NOT_STARTED -> renderCaptureNotStarted() LiveFeedbackFragmentViewModel.CapturingState.CAPTURING -> renderCapturing() - LiveFeedbackFragmentViewModel.CapturingState.FINISHED -> { mainVm.captureFinished(vm.sortedQualifyingCaptures) findNavController().navigateSafely( @@ -240,39 +251,45 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) } } - private fun renderCapturingStateColors() { - with(binding) { - captureOverlay.drawWhiteTarget() - captureFeedbackTxtExplanation.setTextColor( - ContextCompat.getColor(requireContext(), IDR.color.simprints_blue_grey), - ) - } - } - - private fun renderCapturingNotStarted() { + private fun renderCaptureNotStarted() { binding.apply { + if (vm.isAutoCapture) { + captureFeedbackBtn.setText(IDR.string.face_capture_start_capture) + captureFeedbackBtn.isChecked = true + } else { + captureFeedbackBtn.setText(IDR.string.face_capture_title_previewing) + } + captureOverlay.drawSemiTransparentTarget() - captureFeedbackBtn.setText(IDR.string.face_capture_title_previewing) captureFeedbackBtn.isVisible = true captureFeedbackPermissionButton.isGone = true + setManualCaptureButtonClickable(false) } - toggleCaptureButtons(false) } private fun renderCapturing() { - renderCapturingStateColors() binding.apply { + captureOverlay.drawWhiteTarget() + captureFeedbackTxtExplanation.setTextColor( + ContextCompat.getColor(requireContext(), IDR.color.simprints_blue_grey), + ) + captureProgress.isVisible = true captureFeedbackBtn.setText(IDR.string.face_capture_prep_begin_button_capturing) captureFeedbackBtn.isVisible = true captureFeedbackPermissionButton.isGone = true + setManualCaptureButtonClickable(false) } - toggleCaptureButtons(false) } private fun renderValidFace() { binding.apply { - captureFeedbackBtn.setText(IDR.string.face_capture_begin_button) + if (vm.isAutoCapture) { + captureFeedbackBtn.setText(IDR.string.face_capture_prep_begin_button_capturing) + } else { + captureFeedbackBtn.setText(IDR.string.face_capture_begin_button) + } + captureFeedbackTxtExplanation.text = null captureFeedbackBtn.isVisible = true captureFeedbackPermissionButton.isGone = true @@ -281,8 +298,8 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) true, ContextCompat.getDrawable(requireContext(), R.drawable.ic_checked_white_18dp), ) + setManualCaptureButtonClickable(false) } - toggleCaptureButtons(true) } private fun renderValidCapturingFace() { @@ -296,9 +313,8 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) true, ContextCompat.getDrawable(requireContext(), R.drawable.ic_checked_white_18dp), ) + renderProgressBar(false) } - - renderProgressBar(true) } private fun renderFaceTooFar() { @@ -309,10 +325,9 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) captureFeedbackPermissionButton.isGone = true captureFeedbackBtn.setCheckedWithLeftDrawable(false) + setManualCaptureButtonClickable(false) + renderProgressBar(false) } - - toggleCaptureButtons(false) - renderProgressBar(false) } private fun renderFaceTooClose() { @@ -323,10 +338,9 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) captureFeedbackPermissionButton.isGone = true captureFeedbackBtn.setCheckedWithLeftDrawable(false) + setManualCaptureButtonClickable(false) + renderProgressBar(false) } - - toggleCaptureButtons(false) - renderProgressBar(false) } private fun renderNoFace() { @@ -337,10 +351,9 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) captureFeedbackPermissionButton.isGone = true captureFeedbackBtn.setCheckedWithLeftDrawable(false) + setManualCaptureButtonClickable(false) + renderProgressBar(false) } - - toggleCaptureButtons(false) - renderProgressBar(false) } private fun renderFaceNotStraight() { @@ -351,10 +364,9 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) captureFeedbackPermissionButton.isGone = true captureFeedbackBtn.setCheckedWithLeftDrawable(false) + setManualCaptureButtonClickable(false) + renderProgressBar(false) } - - toggleCaptureButtons(false) - renderProgressBar(false) } private fun renderBadQuality() { @@ -365,31 +377,20 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) captureFeedbackPermissionButton.isGone = true captureFeedbackBtn.setCheckedWithLeftDrawable(false) + setManualCaptureButtonClickable(false) + renderProgressBar(false) } - - toggleCaptureButtons(false) - renderProgressBar(false) } - private fun renderProgressBar(valid: Boolean) { - binding.apply { - val progressColor = if (valid) { - IDR.color.simprints_green_light - } else { - IDR.color.simprints_blue_grey_light - } - - captureProgress.progressColor = ContextCompat.getColor( - requireContext(), - progressColor, - ) - - captureProgress.value = vm.userCaptures.size.toFloat() - } + private fun FragmentLiveFeedbackBinding.renderProgressBar(validCapture: Boolean) { + captureProgress.progressColor = if (validCapture) validCaptureProgressColor else defaultCaptureProgressColor + captureProgress.value = vm.getNormalizedProgress() } - private fun toggleCaptureButtons(valid: Boolean) { - binding.captureFeedbackBtn.isClickable = valid + private fun FragmentLiveFeedbackBinding.setManualCaptureButtonClickable(clickable: Boolean) { + if (!vm.isAutoCapture) { + captureFeedbackBtn.isClickable = clickable + } } private fun renderNoPermission(shouldOpenSettings: Boolean) { @@ -403,14 +404,14 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) requireActivity().startActivity( Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse("package:${requireActivity().packageName}"), + "package:${requireActivity().packageName}".toUri(), ), ) } else { launchPermissionRequest.launch(Manifest.permission.CAMERA) } } + setManualCaptureButtonClickable(false) } - toggleCaptureButtons(false) } } diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModel.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModel.kt index b2dfb41eb3..40a09058e3 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModel.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModel.kt @@ -9,18 +9,22 @@ import com.simprints.core.tools.time.TimeHelper import com.simprints.face.capture.models.FaceDetection import com.simprints.face.capture.models.FaceTarget import com.simprints.face.capture.models.SymmetricTarget +import com.simprints.face.capture.usecases.IsUsingAutoCaptureUseCase import com.simprints.face.capture.usecases.SimpleCaptureEventReporter import com.simprints.face.infra.basebiosdk.detection.Face import com.simprints.face.infra.basebiosdk.detection.FaceDetector import com.simprints.face.infra.biosdkresolver.ResolveFaceBioSdkUseCase +import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.models.experimental import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.logging.LoggingConstants.CrashReportTag.FACE_CAPTURE import com.simprints.infra.logging.Simber import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.concurrent.atomic.AtomicBoolean @@ -32,6 +36,7 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor( private val configManager: ConfigManager, private val eventReporter: SimpleCaptureEventReporter, private val timeHelper: TimeHelper, + private val isUsingAutoCaptureUseCase: IsUsingAutoCaptureUseCase, ) : ViewModel() { private var attemptNumber: Int = 1 private var samplesToCapture: Int = 1 @@ -54,8 +59,53 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor( val displayCameraFlashControls = MutableLiveData(false) + var isAutoCapture: Boolean = false + + private var captureImagingStartTime: Long = 0 + private var isAutoCaptureHeldOff = true + private var autoCaptureImagingTimeoutJob: Job? = null + private var autoCaptureImagingDurationMillis: Long = FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT private lateinit var faceDetector: FaceDetector + fun initCapture( + bioSdk: FaceConfiguration.BioSdk, + samplesToCapture: Int, + attemptNumber: Int, + ) { + Simber.i("Initialise face detection", tag = FACE_CAPTURE) + this.samplesToCapture = samplesToCapture + this.attemptNumber = attemptNumber + viewModelScope.launch { + faceDetector = resolveFaceBioSdk(bioSdk).detector + + val config = configManager.getProjectConfiguration() + isAutoCapture = isUsingAutoCaptureUseCase(config) + + qualityThreshold = config.face?.getSdkConfiguration(bioSdk)?.qualityThreshold ?: 0f + singleQualityFallbackCaptureRequired = config.experimental().singleQualityFallbackRequired + autoCaptureImagingDurationMillis = config.experimental().faceAutoCaptureImagingDurationMillis + displayCameraFlashControls.postValue(config.experimental().displayCameraFlashToggle) + } + } + + fun holdOffAutoCapture() { + if (isAutoCapture) { + if (capturingState.value != CapturingState.NOT_STARTED) { + return // too late - imaging has already started + } + capturingState.value = CapturingState.NOT_STARTED // reset view + isAutoCaptureHeldOff = true + } + } + + fun startCapture() { + if (isAutoCapture) { + isAutoCaptureHeldOff = false + } else { + capturingState.value = CapturingState.CAPTURING + } + } + /** * Processes the image * @@ -69,14 +119,34 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor( faceDetection.detectionStartTime = captureStartTime faceDetection.detectionEndTime = timeHelper.now() - currentDetection.postValue(faceDetection) + if (isAutoCapture) { + if (!isAutoCaptureHeldOff) { + currentDetection.postValue(faceDetection) + if (faceDetection.status == FaceDetection.Status.VALID && capturingState.value == CapturingState.NOT_STARTED) { + capturingState.postValue(CapturingState.CAPTURING) + captureImagingStartTime = captureStartTime.ms + autoCaptureImagingTimeoutJob = viewModelScope.launch { + delay(autoCaptureImagingDurationMillis) + finishCapture(attemptNumber) + } + } + } + } else { + currentDetection.postValue(faceDetection) + } when (capturingState.value) { CapturingState.NOT_STARTED -> updateFallbackCaptureIfValid(faceDetection) CapturingState.CAPTURING -> { - userCaptures += faceDetection - if (userCaptures.size == samplesToCapture) { - finishCapture(attemptNumber) + if (isAutoCapture) { + if (isQualifying(faceDetection)) { + updateUserCapturesWith(faceDetection) + } + } else { + userCaptures.add(faceDetection) + if (userCaptures.size == samplesToCapture) { + finishCapture(attemptNumber) + } } } @@ -85,27 +155,37 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor( } } - fun initCapture( - bioSdk: FaceConfiguration.BioSdk, - samplesToCapture: Int, - attemptNumber: Int, - ) { - Simber.i("Initialise face detection", tag = FACE_CAPTURE) - - this.samplesToCapture = samplesToCapture - this.attemptNumber = attemptNumber - viewModelScope.launch { - faceDetector = resolveFaceBioSdk(bioSdk).detector + fun getNormalizedProgress(): Float = if (isAutoCapture) { + ((timeHelper.now().ms - captureImagingStartTime).toFloat() / autoCaptureImagingDurationMillis).coerceIn(0f, 1f) + } else { + userCaptures.size.toFloat() / samplesToCapture + } - val config = configManager.getProjectConfiguration() - qualityThreshold = config.face?.getSdkConfiguration(bioSdk)?.qualityThreshold ?: 0f - singleQualityFallbackCaptureRequired = config.experimental().singleQualityFallbackRequired - displayCameraFlashControls.postValue(config.experimental().displayCameraFlashToggle) + private fun isQualifying(faceDetection: FaceDetection): Boolean { + if (autoCaptureImagingTimeoutJob?.isActive != true) { + return false + } + if (!faceDetection.hasValidStatus()) { + return false + } + val betterPreviousCaptureCount = userCaptures.count { previousCapture -> + (previousCapture.face?.quality ?: -1f) > (faceDetection.face?.quality ?: -1f) } + return betterPreviousCaptureCount < samplesToCapture } - fun startCapture() { - capturingState.value = CapturingState.CAPTURING + private fun updateUserCapturesWith(faceDetection: FaceDetection) { + if (userCaptures.count() == samplesToCapture) { + userCaptures.indices + .minByOrNull { index -> + userCaptures[index].face?.quality ?: -1f + }?.takeIf { it >= 0 } + ?.let { worseQualityCaptureIndex -> + userCaptures[worseQualityCaptureIndex] = faceDetection + } + } else { + userCaptures.add(faceDetection) + } } /** @@ -115,11 +195,11 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor( Simber.i("Finish capture", tag = FACE_CAPTURE) viewModelScope.launch { sortedQualifyingCaptures = userCaptures - .filter { it.hasValidStatus() } + .filter { isAutoCapture || it.hasValidStatus() } // Auto-capture images are pre-qualified .sortedByDescending { it.face?.quality } .ifEmpty { listOfNotNull(fallbackCapture) } - sendAllCaptureEvents(attemptNumber) + sendCaptureEvents(attemptNumber) capturingState.postValue(CapturingState.FINISHED) } @@ -198,7 +278,7 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor( * Since events are saved in a blocking way in [SimpleCaptureEventReporter.addCaptureEvents], * to speed things up this method creates multiple async jobs and run them all in parallel. */ - private fun sendAllCaptureEvents(attemptNumber: Int) = runBlocking { + private fun sendCaptureEvents(attemptNumber: Int) = runBlocking { userCaptures .map { async { sendCaptureEvent(it, attemptNumber) } } .plus(async { sendCaptureEvent(fallbackCapture, attemptNumber) }) @@ -210,7 +290,7 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor( attemptNumber: Int, ) { if (faceDetection == null) return - eventReporter.addCaptureEvents(faceDetection, attemptNumber, qualityThreshold) + eventReporter.addCaptureEvents(faceDetection, attemptNumber, qualityThreshold, isAutoCapture = isAutoCapture) } enum class CapturingState { NOT_STARTED, CAPTURING, FINISHED } diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragment.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragment.kt deleted file mode 100644 index e0ad0c1862..0000000000 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragment.kt +++ /dev/null @@ -1,406 +0,0 @@ -package com.simprints.face.capture.screens.livefeedbackautocapture - -import android.Manifest -import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri -import android.os.Bundle -import android.provider.Settings -import android.util.Size -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.camera.core.CameraControl -import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA -import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888 -import androidx.camera.core.Preview -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.camera.lifecycle.awaitInstance -import androidx.core.content.ContextCompat -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import com.simprints.core.domain.permission.PermissionStatus -import com.simprints.core.tools.extentions.hasCameraFlash -import com.simprints.core.tools.extentions.hasPermission -import com.simprints.core.tools.extentions.permissionFromResult -import com.simprints.face.capture.R -import com.simprints.face.capture.databinding.FragmentLiveFeedbackAutoCaptureBinding -import com.simprints.face.capture.models.FaceDetection -import com.simprints.face.capture.screens.FaceCaptureViewModel -import com.simprints.face.capture.screens.livefeedback.CropToTargetOverlayAnalyzer -import com.simprints.infra.logging.Simber -import com.simprints.infra.uibase.navigation.navigateSafely -import com.simprints.infra.uibase.view.applySystemBarInsets -import com.simprints.infra.uibase.view.setCheckedWithLeftDrawable -import com.simprints.infra.uibase.viewbinding.viewBinding -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import com.simprints.infra.resources.R as IDR - -/** - * As the user is capturing subject's face, they are presented with this fragment, which displays - * live information about distance and whether the face is ready to be captured or not. - * It also displays the capture process of the face and then sends this result to - * [com.simprints.face.capture.screens.confirmation.ConfirmationFragment] - */ -@AndroidEntryPoint -internal class LiveFeedbackAutoCaptureFragment : Fragment(R.layout.fragment_live_feedback_auto_capture) { - /** Blocking camera operations are performed using this executor */ - private lateinit var cameraExecutor: ExecutorService - - private val mainVm: FaceCaptureViewModel by activityViewModels() - - private val vm: LiveFeedbackAutoCaptureFragmentViewModel by viewModels() - private val binding by viewBinding(FragmentLiveFeedbackAutoCaptureBinding::bind) - - private lateinit var screenSize: Size - private lateinit var targetResolution: Size - - private var cameraControl: CameraControl? = null - - private val launchPermissionRequest = registerForActivityResult( - ActivityResultContracts.RequestPermission(), - ) { granted -> - when (requireActivity().permissionFromResult(Manifest.permission.CAMERA, granted)) { - PermissionStatus.Granted -> setUpCamera() - PermissionStatus.Denied -> renderNoPermission(false) - PermissionStatus.DeniedNeverAskAgain -> renderNoPermission(true) - } - } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - applySystemBarInsets(view) - initFragment() - } - - private fun initFragment() { - screenSize = with(resources.displayMetrics) { Size(widthPixels, widthPixels) } - bindViewModel() - binding.captureProgress.max = 1 // normalized progress - - binding.captureFeedbackBtn.setOnClickListener { - vm.startCapture() - binding.captureFeedbackBtn.isClickable = false - } - - // Wait till the views gets its final size then init frame processor and setup the camera - binding.faceCaptureCamera.post { - if (view != null) { - vm.initCapture(mainVm.bioSDK, mainVm.samplesToCapture, mainVm.attemptNumber) - } - } - - binding.captureInstructionsBtn.setOnClickListener { - findNavController().navigateSafely( - currentFragment = this, - directions = LiveFeedbackAutoCaptureFragmentDirections.actionFaceLiveFeedbackFragmentToFacePreparationFragment(), - ) - } - - with(binding.captureFlashButton) { - isSelected = false - setOnClickListener { - val torchEnabled = !binding.captureFlashButton.isSelected - toggleTorche(torchEnabled) - } - } - } - - private fun toggleTorche(enabled: Boolean) { - cameraControl?.enableTorch(enabled) - binding.captureFlashButton.isSelected = enabled - } - - /** Initialize CameraX, and prepare to bind the camera use cases */ - private fun setUpCamera() = lifecycleScope.launch { - if (::cameraExecutor.isInitialized && !cameraExecutor.isShutdown) { - return@launch - } - vm.holdOffCapture() - binding.captureFeedbackBtn.isClickable = true - - // Initialize our background executor - cameraExecutor = Executors.newSingleThreadExecutor() - // ImageAnalysis - // Todo choose accurate output image resolution that respects quality,performance and face analysis SDKs https://simprints.atlassian.net/browse/CORE-2569 - if (!::targetResolution.isInitialized) { - targetResolution = Size(binding.captureOverlay.width, binding.captureOverlay.height) - } - - val imageAnalyzer = ImageAnalysis - .Builder() - .setTargetResolution(targetResolution) - .setOutputImageRotationEnabled(true) - .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888) - .build() - val cropAnalyzer = CropToTargetOverlayAnalyzer(binding.captureOverlay, ::analyze) - - imageAnalyzer.setAnalyzer(cameraExecutor, cropAnalyzer) - - // Preview - val preview = Preview.Builder().setTargetResolution(targetResolution).build() - val cameraProvider = ProcessCameraProvider.awaitInstance(requireContext()) - cameraProvider.unbindAll() - val camera = cameraProvider.bindToLifecycle( - this@LiveFeedbackAutoCaptureFragment, - DEFAULT_BACK_CAMERA, - preview, - imageAnalyzer, - ) - cameraControl = camera.cameraControl - // Attach the view's surface provider to preview use case - preview.surfaceProvider = binding.faceCaptureCamera.surfaceProvider - } - - override fun onResume() { - super.onResume() - - when { - requireActivity().hasPermission(Manifest.permission.CAMERA) -> setUpCamera() - mainVm.shouldCheckCameraPermissions.getAndSet(false) -> { - // Check permission in onResume() so that if user left the app to go to Settings - // and give the permission, it's reflected when they come back to SID - if (requireActivity().hasPermission(Manifest.permission.CAMERA)) { - setUpCamera() - } else { - launchPermissionRequest.launch(Manifest.permission.CAMERA) - } - } - - else -> mainVm.shouldCheckCameraPermissions.set(true) - } - } - - override fun onStop() { - toggleTorche(false) - // Shut down our background executor - if (::cameraExecutor.isInitialized) { - cameraExecutor.shutdown() - } - super.onStop() - } - - private fun bindViewModel() { - vm.displayCameraFlashControls.observe(viewLifecycleOwner) { - binding.captureFlashButton.isVisible = it && requireContext().hasCameraFlash - } - - vm.currentDetection.observe(viewLifecycleOwner) { - renderCurrentDetection(it) - } - - vm.capturingState.observe(viewLifecycleOwner) { - @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") when (it) { - LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.NOT_STARTED -> renderCapturingNotStarted() - - LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.CAPTURING -> renderCapturing() - - LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.FINISHED -> { - mainVm.captureFinished(vm.sortedQualifyingCaptures) - findNavController().navigateSafely( - currentFragment = this, - directions = LiveFeedbackAutoCaptureFragmentDirections.actionFaceLiveFeedbackFragmentToFaceConfirmationFragment(), - ) - } - } - } - } - - private fun analyze(image: Bitmap) { - try { - vm.process(croppedBitmap = image) - } catch (t: Throwable) { - Simber.e("Image analysis crashed", t) - // Image analysis is running in bg thread - lifecycleScope.launch { - mainVm.submitError(t) - } - } - } - - private fun renderCurrentDetection(faceDetection: FaceDetection) { - when (faceDetection.status) { - FaceDetection.Status.NOFACE -> renderNoFace() - FaceDetection.Status.OFFYAW -> renderFaceNotStraight() - FaceDetection.Status.OFFROLL -> renderFaceNotStraight() - FaceDetection.Status.TOOCLOSE -> renderFaceTooClose() - FaceDetection.Status.TOOFAR -> renderFaceTooFar() - FaceDetection.Status.BAD_QUALITY -> renderBadQuality() - FaceDetection.Status.VALID -> renderValidFace() - FaceDetection.Status.VALID_CAPTURING -> renderValidCapturingFace() - } - } - - private fun renderCapturingStateColors() { - with(binding) { - captureOverlay.drawWhiteTarget() - captureFeedbackTxtExplanation.setTextColor( - ContextCompat.getColor(requireContext(), IDR.color.simprints_blue_grey), - ) - } - } - - private fun renderCapturingNotStarted() { - binding.apply { - captureOverlay.drawSemiTransparentTarget() - captureFeedbackBtn.setText(IDR.string.face_capture_start_capture) - captureFeedbackBtn.isVisible = true - captureFeedbackBtn.isChecked = true - captureFeedbackPermissionButton.isGone = true - } - } - - private fun renderCapturing() { - renderCapturingStateColors() - binding.apply { - captureProgress.isVisible = true - captureFeedbackBtn.setText(IDR.string.face_capture_prep_begin_button_capturing) - captureFeedbackBtn.isVisible = true - captureFeedbackPermissionButton.isGone = true - } - } - - private fun renderValidFace() { - binding.apply { - captureFeedbackBtn.setText(IDR.string.face_capture_prep_begin_button_capturing) - captureFeedbackTxtExplanation.text = null - captureFeedbackBtn.isVisible = true - captureFeedbackPermissionButton.isGone = true - - captureFeedbackBtn.setCheckedWithLeftDrawable( - true, - ContextCompat.getDrawable(requireContext(), R.drawable.ic_checked_white_18dp), - ) - } - } - - private fun renderValidCapturingFace() { - binding.apply { - captureFeedbackBtn.setText(IDR.string.face_capture_prep_begin_button_capturing) - captureFeedbackTxtExplanation.setText(IDR.string.face_capture_hold) - captureFeedbackBtn.isVisible = true - captureFeedbackPermissionButton.isGone = true - - captureFeedbackBtn.setCheckedWithLeftDrawable( - true, - ContextCompat.getDrawable(requireContext(), R.drawable.ic_checked_white_18dp), - ) - } - - renderProgressBar(true) - } - - private fun renderFaceTooFar() { - binding.apply { - captureFeedbackBtn.setText(IDR.string.face_capture_title_too_far) - captureFeedbackTxtExplanation.setText(IDR.string.face_capture_error_too_far) - captureFeedbackBtn.isVisible = true - captureFeedbackPermissionButton.isGone = true - - captureFeedbackBtn.setCheckedWithLeftDrawable(false) - } - - renderProgressBar(false) - } - - private fun renderFaceTooClose() { - binding.apply { - captureFeedbackBtn.setText(IDR.string.face_capture_title_too_close) - captureFeedbackTxtExplanation.setText(IDR.string.face_capture_error_too_close) - captureFeedbackBtn.isVisible = true - captureFeedbackPermissionButton.isGone = true - - captureFeedbackBtn.setCheckedWithLeftDrawable(false) - } - - renderProgressBar(false) - } - - private fun renderNoFace() { - binding.apply { - captureFeedbackBtn.setText(IDR.string.face_capture_title_no_face) - captureFeedbackTxtExplanation.setText(IDR.string.face_capture_error_no_face) - captureFeedbackBtn.isVisible = true - captureFeedbackPermissionButton.isGone = true - - captureFeedbackBtn.setCheckedWithLeftDrawable(false) - } - - renderProgressBar(false) - } - - private fun renderFaceNotStraight() { - binding.apply { - captureFeedbackBtn.setText(IDR.string.face_capture_title_look_straight) - captureFeedbackTxtExplanation.setText(IDR.string.face_capture_error_look_straight) - captureFeedbackBtn.isVisible = true - captureFeedbackPermissionButton.isGone = true - - captureFeedbackBtn.setCheckedWithLeftDrawable(false) - } - - renderProgressBar(false) - } - - private fun renderBadQuality() { - binding.apply { - captureFeedbackBtn.setText(IDR.string.face_capture_title_bad_quality) - captureFeedbackTxtExplanation.setText(IDR.string.face_capture_error_bad_quality) - captureFeedbackBtn.isVisible = true - captureFeedbackPermissionButton.isGone = true - - captureFeedbackBtn.setCheckedWithLeftDrawable(false) - } - - renderProgressBar(false) - } - - private fun renderProgressBar(valid: Boolean) { - binding.apply { - val progressColor = if (valid) { - IDR.color.simprints_green_light - } else { - IDR.color.simprints_blue_grey_light - } - - captureProgress.progressColor = ContextCompat.getColor( - requireContext(), - progressColor, - ) - - captureProgress.value = vm.getAutoCaptureImagingProgressNormalized() - } - } - - private fun renderNoPermission(shouldOpenSettings: Boolean) { - binding.apply { - captureOverlay.drawSemiTransparentTarget() - captureFeedbackTxtExplanation.setText(IDR.string.face_capture_permission_denied) - captureFeedbackBtn.isGone = true - captureFeedbackPermissionButton.isVisible = true - captureFeedbackPermissionButton.setOnClickListener { - if (shouldOpenSettings) { - requireActivity().startActivity( - Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse("package:${requireActivity().packageName}"), - ), - ) - } else { - launchPermissionRequest.launch(Manifest.permission.CAMERA) - } - } - } - } -} diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModel.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModel.kt deleted file mode 100644 index e5fe54b602..0000000000 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModel.kt +++ /dev/null @@ -1,275 +0,0 @@ -package com.simprints.face.capture.screens.livefeedbackautocapture - -import android.graphics.Bitmap -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.simprints.core.tools.extentions.area -import com.simprints.core.tools.time.TimeHelper -import com.simprints.face.capture.models.FaceDetection -import com.simprints.face.capture.models.FaceTarget -import com.simprints.face.capture.models.SymmetricTarget -import com.simprints.face.capture.usecases.SimpleCaptureEventReporter -import com.simprints.face.infra.basebiosdk.detection.Face -import com.simprints.face.infra.basebiosdk.detection.FaceDetector -import com.simprints.face.infra.biosdkresolver.ResolveFaceBioSdkUseCase -import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT -import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.experimental -import com.simprints.infra.config.sync.ConfigManager -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject - -@HiltViewModel -internal class LiveFeedbackAutoCaptureFragmentViewModel @Inject constructor( - private val resolveFaceBioSdk: ResolveFaceBioSdkUseCase, - private val configManager: ConfigManager, - private val eventReporter: SimpleCaptureEventReporter, - private val timeHelper: TimeHelper, -) : ViewModel() { - private var attemptNumber: Int = 1 - private var samplesToKeep: Int = 1 - private var qualityThreshold: Float = 0f - private var singleQualityFallbackCaptureRequired: Boolean = false - private var captureImagingStartTime: Long = 0 - - private val faceTarget = FaceTarget( - SymmetricTarget(VALID_YAW_DELTA), - SymmetricTarget(VALID_ROLL_DELTA), - 0.20f..0.5f, - ) - private val fallbackCaptureEventStartTime = timeHelper.now() - private var shouldSendFallbackCaptureEvent: AtomicBoolean = AtomicBoolean(true) - private var fallbackCapture: FaceDetection? = null - - val userCaptures = mutableListOf() - var sortedQualifyingCaptures = listOf() - val currentDetection = MutableLiveData() - val capturingState = MutableLiveData(CapturingState.NOT_STARTED) - - val displayCameraFlashControls = MutableLiveData(false) - - private var isAutoCaptureHeldOff = true - private var autoCaptureImagingTimeoutJob: Job? = null - private var autoCaptureImagingDurationMillis: Long = FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT - private lateinit var faceDetector: FaceDetector - - fun holdOffCapture() { - if (capturingState.value != CapturingState.NOT_STARTED) { - return // too late - imaging has already started - } - capturingState.value = CapturingState.NOT_STARTED // reset view - isAutoCaptureHeldOff = true - } - - fun startCapture() { - isAutoCaptureHeldOff = false - } - - /** - * Processes the image - * - * @param image is the camera frame - */ - fun process(croppedBitmap: Bitmap) { - val captureStartTime = timeHelper.now() - val potentialFace = faceDetector.analyze(croppedBitmap) - - val faceDetection = getFaceDetectionFromPotentialFace(croppedBitmap, potentialFace) - faceDetection.detectionStartTime = captureStartTime - faceDetection.detectionEndTime = timeHelper.now() - - if (!isAutoCaptureHeldOff) { - currentDetection.postValue(faceDetection) - if (faceDetection.status == FaceDetection.Status.VALID && - capturingState.value == CapturingState.NOT_STARTED - ) { - capturingState.postValue(CapturingState.CAPTURING) - captureImagingStartTime = captureStartTime.ms - autoCaptureImagingTimeoutJob = viewModelScope.launch { - delay(autoCaptureImagingDurationMillis) - finishCapture(attemptNumber) - } - } - } - - when (capturingState.value) { - CapturingState.NOT_STARTED -> updateFallbackCaptureIfValid(faceDetection) - CapturingState.CAPTURING -> { - if (isQualifying(faceDetection)) { - updateUserCapturesWith(faceDetection) - } - } - - else -> { // no-op - } - } - } - - fun initCapture( - bioSdk: FaceConfiguration.BioSdk, - samplesToKeep: Int, - attemptNumber: Int, - ) { - this.samplesToKeep = samplesToKeep - this.attemptNumber = attemptNumber - viewModelScope.launch { - faceDetector = resolveFaceBioSdk(bioSdk).detector - - val config = configManager.getProjectConfiguration() - qualityThreshold = config.face?.getSdkConfiguration(bioSdk)?.qualityThreshold ?: 0f - singleQualityFallbackCaptureRequired = config.experimental().singleQualityFallbackRequired - autoCaptureImagingDurationMillis = config.experimental().faceAutoCaptureImagingDurationMillis - displayCameraFlashControls.postValue(config.experimental().displayCameraFlashToggle) - } - } - - fun getAutoCaptureImagingProgressNormalized(): Float = - ((timeHelper.now().ms - captureImagingStartTime).toFloat() / autoCaptureImagingDurationMillis).coerceIn(0f, 1f) - - private fun isQualifying(faceDetection: FaceDetection): Boolean { - if (autoCaptureImagingTimeoutJob?.isActive != true) { - return false - } - if (!faceDetection.hasValidStatus()) { - return false - } - val betterPreviousCaptureCount = userCaptures.count { previousCapture -> - (previousCapture.face?.quality ?: -1f) > (faceDetection.face?.quality ?: -1f) - } - return betterPreviousCaptureCount < samplesToKeep - } - - private fun updateUserCapturesWith(faceDetection: FaceDetection) { - if (userCaptures.count() == samplesToKeep) { - userCaptures.indices - .minByOrNull { index -> - userCaptures[index].face?.quality ?: -1f - }?.takeIf { it >= 0 } - ?.let { worseQualityCaptureIndex -> - userCaptures[worseQualityCaptureIndex] = faceDetection - } - } else { - userCaptures.add(faceDetection) - } - } - - /** - * If any of the user captures are good, use them. If not, use the fallback capture. - */ - private fun finishCapture(attemptNumber: Int) { - viewModelScope.launch { - sortedQualifyingCaptures = userCaptures - .sortedByDescending { it.face?.quality } - .ifEmpty { listOfNotNull(fallbackCapture) } - - sendQualifyingAndFallbackCaptureEvents(attemptNumber) - - capturingState.postValue(CapturingState.FINISHED) - } - } - - private fun getFaceDetectionFromPotentialFace( - bitmap: Bitmap, - potentialFace: Face?, - ): FaceDetection = if (potentialFace == null) { - bitmap.recycle() - FaceDetection( - bitmap = bitmap, - face = null, - status = FaceDetection.Status.NOFACE, - detectionStartTime = timeHelper.now(), - detectionEndTime = timeHelper.now(), - ) - } else { - getFaceDetection(bitmap, potentialFace) - } - - private fun getFaceDetection( - bitmap: Bitmap, - potentialFace: Face, - ): FaceDetection { - val areaOccupied = potentialFace.relativeBoundingBox.area() - val status = when { - areaOccupied < faceTarget.areaRange.start -> FaceDetection.Status.TOOFAR - areaOccupied > faceTarget.areaRange.endInclusive -> FaceDetection.Status.TOOCLOSE - potentialFace.yaw !in faceTarget.yawTarget -> FaceDetection.Status.OFFYAW - potentialFace.roll !in faceTarget.rollTarget -> FaceDetection.Status.OFFROLL - shouldCheckQuality() && potentialFace.quality < qualityThreshold -> FaceDetection.Status.BAD_QUALITY - capturingState.value == CapturingState.CAPTURING -> FaceDetection.Status.VALID_CAPTURING - else -> FaceDetection.Status.VALID - } - - return FaceDetection( - bitmap = bitmap, - face = potentialFace, - status = status, - detectionStartTime = timeHelper.now(), - detectionEndTime = timeHelper.now(), - ) - } - - private fun shouldCheckQuality() = !singleQualityFallbackCaptureRequired || fallbackCapture == null - - /** - * While the user has not started the capture flow, we save fallback images. If the capture doesn't - * get any good images, at least one good image will be saved - */ - private fun updateFallbackCaptureIfValid(faceDetection: FaceDetection) { - val fallbackQuality = fallbackCapture?.face?.quality ?: -1f // To ensure that detection is better with defaults - val detectionQuality = faceDetection.face?.quality ?: 0f - - if (faceDetection.hasValidStatus() && detectionQuality >= fallbackQuality) { - fallbackCapture = faceDetection.apply { isFallback = true } - createFirstFallbackCaptureEvent(faceDetection) - } - } - - /** - * Send a fallback capture event only once - */ - private fun createFirstFallbackCaptureEvent(faceDetection: FaceDetection) { - if (shouldSendFallbackCaptureEvent.getAndSet(false)) { - eventReporter.addFallbackCaptureEvent( - fallbackCaptureEventStartTime, - faceDetection.detectionEndTime, - ) - } - } - - /** - * Since events are saved in a blocking way in [SimpleCaptureEventReporter.addCaptureEvents], - * to speed things up this method creates multiple async jobs and run them all in parallel. - * - * Auto-capture generates a number of sample captures that typically significantly exceeds samplesToKeep. - * Thus the non-qualifying samples are excluded to avoid excessive event data. - */ - private fun sendQualifyingAndFallbackCaptureEvents(attemptNumber: Int) = runBlocking { - userCaptures - .map { async { sendCaptureEvent(it, attemptNumber) } } - .plus(async { sendCaptureEvent(fallbackCapture, attemptNumber) }) - .awaitAll() - } - - private suspend fun sendCaptureEvent( - faceDetection: FaceDetection?, - attemptNumber: Int, - ) { - if (faceDetection == null) return - eventReporter.addCaptureEvents(faceDetection, attemptNumber, qualityThreshold, isAutoCapture = true) - } - - enum class CapturingState { NOT_STARTED, CAPTURING, FINISHED } - - companion object { - private const val VALID_ROLL_DELTA = 15f - private const val VALID_YAW_DELTA = 30f - } -} diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/README.md b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/README.md deleted file mode 100644 index f5def99d18..0000000000 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Face auto-capture - -This `livefeedbackautocapture` package is similar to the manual capture livefeedback one, except: - -* Auto-capture has a preparation period from the moment `LiveFeedbackAutoCaptureFragment` becomes visible and for - `LiveFeedbackAutoCaptureFragmentViewModel.AUTO_CAPTURE_VIEWFINDER_RESUME_DELAY_MS` amount of time. This helps prevent startling the user - with a too early start of auto-capture - before the user would aim the viewfinder at a face. -* The auto-capture imaging progress starts when a qualifying face image appears in the viewfinder, and lasts for - `LiveFeedbackAutoCaptureFragmentViewModel.AUTO_CAPTURE_IMAGING_DURATION_MS` amount of time. -* The number of images to accept is `LiveFeedbackAutoCaptureFragmentViewModel.samplesToKeep` - similarly to - `LiveFeedbackFragmentViewModel.samplesToCapture`. The best quality sampled images are selected to keep. -* The kept sampled images and the fallback image are included in the capture events - unlike in `LiveFeedbackFragmentViewModel` where all - conventionally captured and the fallback images are included. -* The capture events have an `isAutoCapture` flag value of `true`. - -The reason for having this package as a near-copy of `livefeedback` is to keep both implementations independent but similar. The layout for -`LiveFeedbackAutoCaptureFragment` is `fragment_live_feedback_auto_capture.xml`. diff --git a/face/capture/src/main/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCase.kt b/face/capture/src/main/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCase.kt index 34e22f46b8..367a717ac0 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCase.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCase.kt @@ -2,25 +2,25 @@ package com.simprints.face.capture.usecases import android.content.Context import androidx.preference.PreferenceManager +import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.experimental -import com.simprints.infra.config.sync.ConfigManager import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @Singleton class IsUsingAutoCaptureUseCase @Inject constructor( - private val configManager: ConfigManager, @ApplicationContext private val context: Context, ) { private val preference = PreferenceManager.getDefaultSharedPreferences(context) - suspend operator fun invoke(): Boolean { - val isFeatureEnabled = configManager.getProjectConfiguration().experimental().faceAutoCaptureEnabled - val isOptionTurnedOnInSettings = preference.getBoolean(AUTO_CAPTURE_PREFERENCE_KEY, true) - return isFeatureEnabled && isOptionTurnedOnInSettings + operator fun invoke(projectConfiguration: ProjectConfiguration): Boolean { + val isFeatureEnabled = projectConfiguration.experimental().faceAutoCaptureEnabled + return isFeatureEnabled && isOptionTurnedOnInSettings() } + private fun isOptionTurnedOnInSettings(): Boolean = preference.getBoolean(AUTO_CAPTURE_PREFERENCE_KEY, true) + companion object { private const val AUTO_CAPTURE_PREFERENCE_KEY = "preference_enable_face_auto_capture" } diff --git a/face/capture/src/main/res/layout-land/fragment_live_feedback_auto_capture.xml b/face/capture/src/main/res/layout-land/fragment_live_feedback_auto_capture.xml deleted file mode 100644 index 43b9b7ec0e..0000000000 --- a/face/capture/src/main/res/layout-land/fragment_live_feedback_auto_capture.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/face/capture/src/main/res/layout/fragment_live_feedback_auto_capture.xml b/face/capture/src/main/res/layout/fragment_live_feedback_auto_capture.xml deleted file mode 100644 index ddb41db106..0000000000 --- a/face/capture/src/main/res/layout/fragment_live_feedback_auto_capture.xml +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/face/capture/src/main/res/navigation/graph_face_capture_auto_internal.xml b/face/capture/src/main/res/navigation/graph_face_capture_auto_internal.xml deleted file mode 100644 index 129be9d9cd..0000000000 --- a/face/capture/src/main/res/navigation/graph_face_capture_auto_internal.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt index 3f143e143b..f736f74cc2 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt @@ -1,7 +1,7 @@ package com.simprints.face.capture.screens import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.* import com.simprints.core.tools.time.Timestamp import com.simprints.face.capture.models.FaceDetection import com.simprints.face.capture.usecases.BitmapToByteArrayUseCase @@ -25,16 +25,9 @@ import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.assertEventNotReceived import com.simprints.testtools.common.livedata.assertEventReceived import com.simprints.testtools.common.livedata.getOrAwaitValue -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coJustRun -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* +import io.mockk.impl.annotations.* import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify import kotlinx.coroutines.flow.flowOf import org.junit.Before import org.junit.Rule @@ -71,9 +64,6 @@ class FaceCaptureViewModelTest { @MockK private lateinit var saveLicenseCheckEvent: SaveLicenseCheckEventUseCase - @MockK - private lateinit var isUsingAutoCapture: IsUsingAutoCaptureUseCase - @MockK private lateinit var shouldShowInstructionsScreen: ShouldShowInstructionsScreenUseCase @@ -104,7 +94,6 @@ class FaceCaptureViewModelTest { coEvery { this@mockk(any()).initializer } returns faceBioSdkInitializer }, saveLicenseCheckEvent, - isUsingAutoCapture, shouldShowInstructionsScreen, "deviceId", ) @@ -314,30 +303,6 @@ class FaceCaptureViewModelTest { assertThat(licenseStatusSlot.captured).isEqualTo(LicenseStatus.VALID) } - @Test - fun `auto-capture should be enabled if it is used according to its use case`() { - // Given - coEvery { isUsingAutoCapture() } returns true - - // When - viewModel.setupAutoCapture() - - // Then - assertThat(viewModel.isAutoCaptureEnabled.getOrAwaitValue()).isTrue() - } - - @Test - fun `auto-capture should be disabled if it is not used according to its use case`() { - // Given - coEvery { isUsingAutoCapture() } returns false - - // When - viewModel.setupAutoCapture() - - // Then - assertThat(viewModel.isAutoCaptureEnabled.getOrAwaitValue()).isFalse() - } - @Test fun `preparation instructions screen should be set to showing according to its use case`() { // Given diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackAutoCaptureFragmentViewModelTest.kt similarity index 66% rename from face/capture/src/test/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModelTest.kt rename to face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackAutoCaptureFragmentViewModelTest.kt index 0bc5310d6f..03215ebebb 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModelTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackAutoCaptureFragmentViewModelTest.kt @@ -1,13 +1,14 @@ -package com.simprints.face.capture.screens.livefeedbackautocapture +package com.simprints.face.capture.screens.livefeedback import android.graphics.Bitmap import android.graphics.Rect import androidx.arch.core.executor.testing.InstantTaskExecutorRule -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.* import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp import com.simprints.face.capture.models.FaceDetection +import com.simprints.face.capture.usecases.IsUsingAutoCaptureUseCase import com.simprints.face.capture.usecases.SimpleCaptureEventReporter import com.simprints.face.infra.basebiosdk.detection.Face import com.simprints.face.infra.basebiosdk.detection.FaceDetector @@ -17,13 +18,8 @@ import com.simprints.infra.config.store.models.experimental import com.simprints.infra.config.sync.ConfigManager import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.testObserver -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.justRun -import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest @@ -60,7 +56,10 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { @MockK lateinit var timeHelper: TimeHelper - private lateinit var viewModel: LiveFeedbackAutoCaptureFragmentViewModel + @MockK + private lateinit var isUsingAutoCapture: IsUsingAutoCaptureUseCase + + private lateinit var viewModel: LiveFeedbackFragmentViewModel @Before fun setUp() { @@ -73,7 +72,10 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { ?.getSdkConfiguration(any()) ?.qualityThreshold } returns QUALITY_THRESHOLD - coEvery { configManager.getProjectConfiguration().experimental().singleQualityFallbackRequired } returns false + every { isUsingAutoCapture.invoke(any()) } returns true + coEvery { + configManager.getProjectConfiguration().experimental().singleQualityFallbackRequired + } returns false every { timeHelper.now() } returnsMany (0..100L).map { Timestamp(it) } justRun { previewFrame.recycle() } val resolveFaceBioSdkUseCase = mockk { @@ -82,11 +84,12 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { } } - viewModel = LiveFeedbackAutoCaptureFragmentViewModel( + viewModel = LiveFeedbackFragmentViewModel( resolveFaceBioSdkUseCase, configManager, eventReporter, timeHelper, + isUsingAutoCapture, ) } @@ -99,10 +102,12 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0) viewModel.process(frame) - assertThat(currentDetection.observedValues) + Truth + .assertThat(currentDetection.observedValues) .isEmpty() - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.NOT_STARTED) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.NOT_STARTED) } @Test @@ -113,13 +118,15 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0) viewModel.startCapture() - viewModel.holdOffCapture() + viewModel.holdOffAutoCapture() viewModel.process(frame) - assertThat(currentDetection.observedValues) + Truth + .assertThat(currentDetection.observedValues) .isEmpty() - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.NOT_STARTED) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.NOT_STARTED) } @Test @@ -132,10 +139,12 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.startCapture() viewModel.process(frame) - assertThat(currentDetection.observedValues.last()?.status) + Truth + .assertThat(currentDetection.observedValues.last()?.status) .isEqualTo(FaceDetection.Status.BAD_QUALITY) - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.NOT_STARTED) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.NOT_STARTED) } @Test @@ -148,9 +157,10 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.startCapture() viewModel.process(frame) - assertThat(currentDetection.observedValues.last()?.hasValidStatus()).isEqualTo(true) - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.CAPTURING) + Truth.assertThat(currentDetection.observedValues.last()?.hasValidStatus()).isEqualTo(true) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.CAPTURING) } @Test @@ -162,12 +172,13 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0) viewModel.startCapture() viewModel.process(frame) - viewModel.holdOffCapture() + viewModel.holdOffAutoCapture() viewModel.process(frame) - assertThat(currentDetection.observedValues.last()?.hasValidStatus()).isEqualTo(true) - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.CAPTURING) + Truth.assertThat(currentDetection.observedValues.last()?.hasValidStatus()).isEqualTo(true) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.CAPTURING) } @Test @@ -184,13 +195,18 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.process(frame) viewModel.process(frame) - assertThat(currentDetection.observedValues.first()?.status) + Truth + .assertThat(currentDetection.observedValues.first()?.status) .isEqualTo(FaceDetection.Status.BAD_QUALITY) - assertThat(capturingState.observedValues.first()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.NOT_STARTED) - assertThat(currentDetection.observedValues.last()?.hasValidStatus()).isEqualTo(true) - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.CAPTURING) + Truth + .assertThat(capturingState.observedValues.first()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.NOT_STARTED) + Truth + .assertThat(currentDetection.observedValues.last()?.hasValidStatus()) + .isEqualTo(true) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.CAPTURING) } @Test @@ -203,7 +219,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.process(frame) val currentDetection = viewModel.currentDetection.testObserver() - assertThat(currentDetection.observedValues.last()?.hasValidStatus()).isEqualTo(true) + Truth.assertThat(currentDetection.observedValues.last()?.hasValidStatus()).isEqualTo(true) coVerify { eventReporter.addFallbackCaptureEvent(any(), any()) } } @@ -218,7 +234,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { advanceTimeBy(AUTO_CAPTURE_IMAGING_DURATION_MS + 1) val currentDetection = viewModel.currentDetection.testObserver() - assertThat(currentDetection.observedValues.last()?.hasValidStatus()).isEqualTo(true) + Truth.assertThat(currentDetection.observedValues.last()?.hasValidStatus()).isEqualTo(true) coVerify { eventReporter.addCaptureEvents(any(), any(), any(), any()) } } @@ -253,12 +269,12 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.process(frame) detections.observedValues.let { - assertThat(it[0]?.status).isEqualTo(FaceDetection.Status.TOOFAR) - assertThat(it[1]?.status).isEqualTo(FaceDetection.Status.TOOCLOSE) - assertThat(it[2]?.status).isEqualTo(FaceDetection.Status.OFFYAW) - assertThat(it[3]?.status).isEqualTo(FaceDetection.Status.OFFROLL) - assertThat(it[4]?.status).isEqualTo(FaceDetection.Status.BAD_QUALITY) - assertThat(it[5]?.status).isEqualTo(FaceDetection.Status.NOFACE) + Truth.assertThat(it[0]?.status).isEqualTo(FaceDetection.Status.TOOFAR) + Truth.assertThat(it[1]?.status).isEqualTo(FaceDetection.Status.TOOCLOSE) + Truth.assertThat(it[2]?.status).isEqualTo(FaceDetection.Status.OFFYAW) + Truth.assertThat(it[3]?.status).isEqualTo(FaceDetection.Status.OFFROLL) + Truth.assertThat(it[4]?.status).isEqualTo(FaceDetection.Status.BAD_QUALITY) + Truth.assertThat(it[5]?.status).isEqualTo(FaceDetection.Status.NOFACE) } coVerify(exactly = 0) { eventReporter.addCaptureEvents(any(), any(), any()) } @@ -269,7 +285,9 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { val validFace: Face = getFace() val badQuality: Face = getFace(quality = -2f) - coEvery { configManager.getProjectConfiguration().experimental().singleQualityFallbackRequired } returns true + coEvery { + configManager.getProjectConfiguration().experimental().singleQualityFallbackRequired + } returns true every { faceDetector.analyze(frame) } returnsMany listOf( badQuality, // not a fallback image due to bad quality @@ -289,8 +307,8 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { detections.observedValues.let { // fallback image frame wasn't observed during preparation delay - assertThat(it[0]?.hasValidStatus()).isEqualTo(true) - assertThat(it[1]?.hasValidStatus()).isEqualTo(true) + Truth.assertThat(it[0]?.hasValidStatus()).isEqualTo(true) + Truth.assertThat(it[1]?.hasValidStatus()).isEqualTo(true) } } @@ -298,7 +316,10 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { fun `Use default imaging duration when not configured`() = runTest { coEvery { faceDetector.analyze(frame) } returns getFace() coEvery { - configManager.getProjectConfiguration().experimental().faceAutoCaptureImagingDurationMillis + configManager + .getProjectConfiguration() + .experimental() + .faceAutoCaptureImagingDurationMillis } returns AUTO_CAPTURE_IMAGING_DURATION_MS val capturingState = viewModel.capturingState.testObserver() @@ -306,31 +327,40 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.startCapture() viewModel.process(frame) advanceTimeBy(AUTO_CAPTURE_IMAGING_DURATION_MS) - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.CAPTURING) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.CAPTURING) advanceTimeBy(1) - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.FINISHED) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.FINISHED) } @Test fun `Use custom imaging duration when provided in config`() = runTest { - val configDuration = 5000 + val configDuration = 5000L coEvery { faceDetector.analyze(frame) } returns getFace() - coEvery { configManager.getProjectConfiguration().custom } returns mapOf("faceAutoCaptureImagingDurationMillis" to configDuration) + coEvery { + configManager + .getProjectConfiguration() + .experimental() + .faceAutoCaptureImagingDurationMillis + } returns configDuration val capturingState = viewModel.capturingState.testObserver() viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0) viewModel.startCapture() viewModel.process(frame) - advanceTimeBy(configDuration.toLong()) - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.CAPTURING) - - advanceTimeBy(1) - assertThat(capturingState.observedValues.last()) - .isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.FINISHED) + advanceTimeBy(configDuration / 2) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.CAPTURING) + + advanceTimeBy(configDuration / 2) + Truth + .assertThat(capturingState.observedValues.last()) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.FINISHED) } @Test @@ -351,33 +381,37 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { currentDetectionObserver.observedValues.let { // 1st frame wasn't observed during preparation delay - assertThat(it[0]?.hasValidStatus()).isEqualTo(true) - assertThat(it[1]?.hasValidStatus()).isEqualTo(true) + Truth.assertThat(it[0]?.hasValidStatus()).isEqualTo(true) + Truth.assertThat(it[1]?.hasValidStatus()).isEqualTo(true) } capturingStateObserver.observedValues.let { - assertThat(it[0]).isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.NOT_STARTED) - assertThat(it[1]).isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.CAPTURING) - assertThat(it[2]).isEqualTo(LiveFeedbackAutoCaptureFragmentViewModel.CapturingState.FINISHED) + Truth + .assertThat(it[0]) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.NOT_STARTED) + Truth + .assertThat(it[1]) + .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.CAPTURING) + Truth.assertThat(it[2]).isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.FINISHED) } - assertThat(viewModel.userCaptures.size).isEqualTo(samplesToKeep) + Truth.assertThat(viewModel.userCaptures.size).isEqualTo(samplesToKeep) viewModel.userCaptures.let { with(it[0]) { - assertThat(hasValidStatus()).isEqualTo(true) - assertThat(face).isEqualTo(validFace) - assertThat(isFallback).isEqualTo(false) + Truth.assertThat(hasValidStatus()).isEqualTo(true) + Truth.assertThat(face).isEqualTo(validFace) + Truth.assertThat(isFallback).isEqualTo(false) } - assertThat(it[1].isFallback).isEqualTo(false) + Truth.assertThat(it[1].isFallback).isEqualTo(false) } with(viewModel.sortedQualifyingCaptures) { - assertThat(size).isEqualTo(samplesToKeep) - assertThat(get(0).face).isEqualTo(validFace) - assertThat(get(0).isFallback).isEqualTo(false) - assertThat(get(1).face).isEqualTo(validFace) - assertThat(get(1).isFallback).isEqualTo(false) + Truth.assertThat(size).isEqualTo(samplesToKeep) + Truth.assertThat(get(0).face).isEqualTo(validFace) + Truth.assertThat(get(0).isFallback).isEqualTo(false) + Truth.assertThat(get(1).face).isEqualTo(validFace) + Truth.assertThat(get(1).isFallback).isEqualTo(false) } coVerify { eventReporter.addFallbackCaptureEvent(any(), any()) } @@ -389,7 +423,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { quality: Float = 1f, yaw: Float = 0f, roll: Float = 0f, - ) = Face(100, 100, rect, yaw, roll, quality, Random.nextBytes(20), "format") + ) = Face(100, 100, rect, yaw, roll, quality, Random.Default.nextBytes(20), "format") companion object { private const val QUALITY_THRESHOLD = -1f diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt index 0ff289eabe..b8b5ba80a6 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt @@ -3,11 +3,12 @@ package com.simprints.face.capture.screens.livefeedback import android.graphics.Bitmap import android.graphics.Rect import androidx.arch.core.executor.testing.InstantTaskExecutorRule -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.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp import com.simprints.face.capture.models.FaceDetection +import com.simprints.face.capture.usecases.IsUsingAutoCaptureUseCase import com.simprints.face.capture.usecases.SimpleCaptureEventReporter import com.simprints.face.infra.basebiosdk.detection.Face import com.simprints.face.infra.basebiosdk.detection.FaceDetector @@ -17,13 +18,8 @@ import com.simprints.infra.config.store.models.experimental import com.simprints.infra.config.sync.ConfigManager import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.testObserver -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.justRun -import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -57,6 +53,8 @@ internal class LiveFeedbackFragmentViewModelTest { @MockK lateinit var timeHelper: TimeHelper + @MockK + private lateinit var isUsingAutoCapture: IsUsingAutoCaptureUseCase private lateinit var viewModel: LiveFeedbackFragmentViewModel @Before @@ -70,6 +68,7 @@ internal class LiveFeedbackFragmentViewModelTest { ?.getSdkConfiguration(any()) ?.qualityThreshold } returns QUALITY_THRESHOLD + every { isUsingAutoCapture.invoke(any()) } returns false coEvery { configManager.getProjectConfiguration().experimental().singleQualityFallbackRequired } returns false every { timeHelper.now() } returnsMany (0..100L).map { Timestamp(it) } justRun { previewFrame.recycle() } @@ -84,6 +83,7 @@ internal class LiveFeedbackFragmentViewModelTest { configManager, eventReporter, timeHelper, + isUsingAutoCapture, ) } diff --git a/face/capture/src/test/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCaseTest.kt b/face/capture/src/test/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCaseTest.kt index 85ce1d0c57..ad18d09dcd 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCaseTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCaseTest.kt @@ -3,13 +3,10 @@ package com.simprints.face.capture.usecases import android.content.Context import android.content.SharedPreferences import androidx.preference.PreferenceManager +import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.experimental -import com.simprints.infra.config.sync.ConfigManager -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.mockkStatic import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -17,9 +14,8 @@ import org.junit.Before import org.junit.Test class IsUsingAutoCaptureUseCaseTest { - @MockK - private lateinit var configManager: ConfigManager + private lateinit var projectConfiguration: ProjectConfiguration @MockK private lateinit var context: Context @@ -35,11 +31,14 @@ class IsUsingAutoCaptureUseCaseTest { mockkStatic(PreferenceManager::class) every { PreferenceManager.getDefaultSharedPreferences(context) } returns sharedPreferences - isUsingAutoCapture = IsUsingAutoCaptureUseCase(configManager, context) + isUsingAutoCapture = IsUsingAutoCaptureUseCase(context) } - private fun setupParams(featureEnabled: Boolean, preferenceEnabled: Boolean) { - coEvery { configManager.getProjectConfiguration().experimental().faceAutoCaptureEnabled } returns featureEnabled + private fun setupParams( + featureEnabled: Boolean, + preferenceEnabled: Boolean, + ) { + coEvery { projectConfiguration.experimental().faceAutoCaptureEnabled } returns featureEnabled every { sharedPreferences.getBoolean("preference_enable_face_auto_capture", true) } returns preferenceEnabled } @@ -49,7 +48,7 @@ class IsUsingAutoCaptureUseCaseTest { setupParams(featureEnabled = true, preferenceEnabled = true) // When - val result = isUsingAutoCapture() + val result = isUsingAutoCapture(projectConfiguration) // Then assertTrue(result) @@ -61,7 +60,7 @@ class IsUsingAutoCaptureUseCaseTest { setupParams(featureEnabled = true, preferenceEnabled = false) // When - val result = isUsingAutoCapture() + val result = isUsingAutoCapture(projectConfiguration) // Then assertFalse(result) @@ -73,7 +72,7 @@ class IsUsingAutoCaptureUseCaseTest { setupParams(featureEnabled = false, preferenceEnabled = true) // When - val result = isUsingAutoCapture() + val result = isUsingAutoCapture(projectConfiguration) // Then assertFalse(result) @@ -85,7 +84,7 @@ class IsUsingAutoCaptureUseCaseTest { setupParams(featureEnabled = false, preferenceEnabled = false) // When - val result = isUsingAutoCapture() + val result = isUsingAutoCapture(projectConfiguration) // Then assertFalse(result)