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 178a7b573f..610574bd57 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 @@ -13,6 +13,7 @@ import com.simprints.core.tools.time.Timestamp import com.simprints.face.capture.FaceCaptureResult import com.simprints.face.capture.models.FaceDetection import com.simprints.face.capture.usecases.BitmapToByteArrayUseCase +import com.simprints.face.capture.usecases.ShouldShowInstructionsScreenUseCase import com.simprints.face.capture.usecases.IsUsingAutoCaptureUseCase import com.simprints.face.capture.usecases.SaveFaceImageUseCase import com.simprints.face.capture.usecases.SimpleCaptureEventReporter @@ -51,6 +52,7 @@ internal class FaceCaptureViewModel @Inject constructor( private val resolveFaceBioSdk: ResolveFaceBioSdkUseCase, private val saveLicenseCheckEvent: SaveLicenseCheckEventUseCase, private val isUsingAutoCapture: IsUsingAutoCaptureUseCase, + private val shouldShowInstructions: ShouldShowInstructionsScreenUseCase, @DeviceID private val deviceID: String, ) : ViewModel() { // Updated in live feedback screen @@ -134,6 +136,9 @@ internal class FaceCaptureViewModel @Inject constructor( _isAutoCaptureEnabled.postValue(isUsingAutoCapture()) } + fun shouldShowInstructionsScreen(): Boolean = + shouldShowInstructions() + private suspend fun initialize( activity: Activity, license: License, 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 fffe77c9c1..011f403c81 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 @@ -119,13 +119,23 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c viewModel.setupAutoCapture() viewModel.isAutoCaptureEnabled.observe(viewLifecycleOwner) { isAutoCaptureEnabled -> - internalNavController?.setGraph( + 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) + } } } 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 55691b6269..3b75d0684f 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 @@ -96,6 +96,13 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) vm.initCapture(mainVm.samplesToCapture, mainVm.attemptNumber) } } + + binding.captureInstructionsBtn.setOnClickListener { + findNavController().navigateSafely( + currentFragment = this, + directions = LiveFeedbackFragmentDirections.actionFaceLiveFeedbackFragmentToFacePreparationFragment(), + ) + } } /** Initialize CameraX, and prepare to bind the camera use cases */ 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 index 4cefc2a5eb..4ed667846a 100644 --- 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 @@ -27,7 +27,7 @@ import com.simprints.core.domain.permission.PermissionStatus 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.FragmentLiveFeedbackBinding +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 @@ -56,7 +56,7 @@ internal class LiveFeedbackAutoCaptureFragment : Fragment(R.layout.fragment_live private val mainVm: FaceCaptureViewModel by activityViewModels() private val vm: LiveFeedbackAutoCaptureFragmentViewModel by viewModels() - private val binding by viewBinding(FragmentLiveFeedbackBinding::bind) + private val binding by viewBinding(FragmentLiveFeedbackAutoCaptureBinding::bind) private lateinit var screenSize: Size private lateinit var targetResolution: Size @@ -96,6 +96,13 @@ internal class LiveFeedbackAutoCaptureFragment : Fragment(R.layout.fragment_live vm.initCapture(mainVm.samplesToCapture, mainVm.attemptNumber) } } + + binding.captureInstructionsBtn.setOnClickListener { + findNavController().navigateSafely( + currentFragment = this, + directions = LiveFeedbackAutoCaptureFragmentDirections.actionFaceLiveFeedbackFragmentToFacePreparationFragment(), + ) + } } /** Initialize CameraX, and prepare to bind the camera use cases */ diff --git a/face/capture/src/main/java/com/simprints/face/capture/usecases/ShouldShowInstructionsScreenUseCase.kt b/face/capture/src/main/java/com/simprints/face/capture/usecases/ShouldShowInstructionsScreenUseCase.kt new file mode 100644 index 0000000000..1e29419e62 --- /dev/null +++ b/face/capture/src/main/java/com/simprints/face/capture/usecases/ShouldShowInstructionsScreenUseCase.kt @@ -0,0 +1,29 @@ +package com.simprints.face.capture.usecases + +import androidx.annotation.VisibleForTesting +import com.simprints.infra.security.SecurityManager +import javax.inject.Inject +import javax.inject.Singleton +import androidx.core.content.edit + +@Singleton +class ShouldShowInstructionsScreenUseCase @Inject constructor( + private val securityManager: SecurityManager, +) { + operator fun invoke(): Boolean { + val sharedPrefs = securityManager.buildEncryptedSharedPreferences(FILENAME_FOR_INSTRUCTIONS_SHOWING_SHARED_PREFS) + val areInstructionsShowing = sharedPrefs.getBoolean(INSTRUCTIONS_SHOWING_PREFERENCE_KEY, true) + if (areInstructionsShowing) { + sharedPrefs.edit { + putBoolean(INSTRUCTIONS_SHOWING_PREFERENCE_KEY, false) + } + } + return areInstructionsShowing + } + + companion object { + private const val FILENAME_FOR_INSTRUCTIONS_SHOWING_SHARED_PREFS = "INSTRUCTIONS_SHOWING" + @VisibleForTesting + const val INSTRUCTIONS_SHOWING_PREFERENCE_KEY = "preference_instructions_showing" + } +} diff --git a/face/capture/src/main/res/color/feedback_instructions_text.xml b/face/capture/src/main/res/color/feedback_instructions_text.xml new file mode 100644 index 0000000000..302c39c06c --- /dev/null +++ b/face/capture/src/main/res/color/feedback_instructions_text.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/face/capture/src/main/res/drawable/feedback_instructions_outline.xml b/face/capture/src/main/res/drawable/feedback_instructions_outline.xml new file mode 100644 index 0000000000..7ee0ad9dfc --- /dev/null +++ b/face/capture/src/main/res/drawable/feedback_instructions_outline.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/face/capture/src/main/res/layout-land/fragment_live_feedback.xml b/face/capture/src/main/res/layout-land/fragment_live_feedback.xml index 1f95b141c0..78bff808de 100644 --- a/face/capture/src/main/res/layout-land/fragment_live_feedback.xml +++ b/face/capture/src/main/res/layout-land/fragment_live_feedback.xml @@ -8,6 +8,24 @@ tools:background="#66000000" tools:ignore="ContentDescription"> + + + + + + 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 index 179d0a180b..6f748f7d76 100644 --- 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 @@ -87,4 +87,22 @@ app:layout_constraintTop_toBottomOf="@id/capture_feedback_txt_explanation" tools:visibility="visible" /> + + 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 index e01cb708a7..129be9d9cd 100644 --- 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 @@ -27,6 +27,11 @@ app:destination="@id/faceConfirmationFragment" app:popUpTo="@+id/graph_face_capture_auto_internal" app:popUpToInclusive="true" /> + + ( @@ -100,6 +104,7 @@ class FaceCaptureViewModelTest { }, saveLicenseCheckEvent, isUsingAutoCapture, + shouldShowInstructionsScreen, "deviceId", ) } @@ -302,6 +307,18 @@ class FaceCaptureViewModelTest { assertThat(viewModel.isAutoCaptureEnabled.getOrAwaitValue()).isFalse() } + @Test + fun `preparation instructions screen should be set to showing according to its use case`() { + // Given + coEvery { shouldShowInstructionsScreen() } returns true + + // When + val isShowing = viewModel.shouldShowInstructionsScreen() + + // Then + assertThat(isShowing).isTrue() + } + @Test fun `test initFaceBioSdk should return error when re-download fails`() { // Given diff --git a/face/capture/src/test/java/com/simprints/face/capture/usecases/ShouldShowInstructionsScreenUseCaseTest.kt b/face/capture/src/test/java/com/simprints/face/capture/usecases/ShouldShowInstructionsScreenUseCaseTest.kt new file mode 100644 index 0000000000..cbc932ec1b --- /dev/null +++ b/face/capture/src/test/java/com/simprints/face/capture/usecases/ShouldShowInstructionsScreenUseCaseTest.kt @@ -0,0 +1,53 @@ +package com.simprints.face.capture.usecases + +import android.content.SharedPreferences +import com.simprints.face.capture.usecases.ShouldShowInstructionsScreenUseCase.Companion.INSTRUCTIONS_SHOWING_PREFERENCE_KEY +import com.simprints.infra.security.SecurityManager +import io.mockk.* +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class ShouldShowInstructionsScreenUseCaseTest { + + @MockK + private lateinit var securityManager: SecurityManager + + @MockK + private lateinit var prefs: SharedPreferences + + private lateinit var shouldShowInstructionsScreen: ShouldShowInstructionsScreenUseCase + + @Before + fun setup() { + MockKAnnotations.init(this, relaxed = true) + every { securityManager.buildEncryptedSharedPreferences(any()) } returns prefs + + shouldShowInstructionsScreen = ShouldShowInstructionsScreenUseCase(securityManager) + } + + @Test + fun `should return true and write false to preferences when instructions are showing`() = runTest { + every { prefs.getBoolean(INSTRUCTIONS_SHOWING_PREFERENCE_KEY, any()) } returns true + val editor = mockk(relaxed = true) + every { prefs.edit() } returns editor + + val result = shouldShowInstructionsScreen() + + assertTrue(result) + verify { editor.putBoolean(INSTRUCTIONS_SHOWING_PREFERENCE_KEY, false) } + } + + @Test + fun `should return false and not write to preferences when instructions are not showing`() = runTest { + every { prefs.getBoolean(INSTRUCTIONS_SHOWING_PREFERENCE_KEY, any()) } returns false + + val result = shouldShowInstructionsScreen() + + assertFalse(result) + verify(exactly = 0) { prefs.edit() } + } +} diff --git a/infra/resources/src/main/res/values/strings.xml b/infra/resources/src/main/res/values/strings.xml index cf4981f5d7..3164eb6c24 100644 --- a/infra/resources/src/main/res/values/strings.xml +++ b/infra/resources/src/main/res/values/strings.xml @@ -172,6 +172,7 @@ Live Capture Preparation Start capture + Instructions Preparing to scan No face detected Make sure the face fills the circle