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 @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -134,6 +136,9 @@ internal class FaceCaptureViewModel @Inject constructor(
_isAutoCaptureEnabled.postValue(isUsingAutoCapture())
}

fun shouldShowInstructionsScreen(): Boolean =
shouldShowInstructions()

private suspend fun initialize(
activity: Activity,
license: License,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/simprints_white" android:state_pressed="true" />
<item android:color="@color/simprints_grey_light" android:state_pressed="false"/>
</selector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<stroke android:width="1dp" android:color="@color/simprints_white" />
<corners android:radius="20dp" />
</shape>
</item>
<item android:state_pressed="false">
<shape>
<stroke android:width="1dp" android:color="@color/simprints_grey_light" />
<corners android:radius="20dp" />
</shape>
</item>
</selector>
18 changes: 18 additions & 0 deletions face/capture/src/main/res/layout-land/fragment_live_feedback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@
tools:background="#66000000"
tools:ignore="ContentDescription">

<com.google.android.material.button.MaterialButton
android:id="@+id/capture_instructions_btn"
style="@style/Text.Subtitle1"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_margin="@dimen/margin_large"
android:background="@drawable/feedback_instructions_outline"
android:paddingHorizontal="@dimen/padding_default"
android:paddingVertical="0dp"
android:includeFontPadding="false"
android:gravity="center"
android:text="@string/face_capture_instructions_title"
android:textColor="@color/feedback_instructions_text"
app:backgroundTint="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

<androidx.camera.view.PreviewView
android:id="@+id/face_capture_camera"
android:layout_width="match_parent"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@
tools:background="#66000000"
tools:ignore="ContentDescription">

<com.google.android.material.button.MaterialButton
android:id="@+id/capture_instructions_btn"
style="@style/Text.Subtitle1"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_margin="@dimen/margin_large"
android:background="@drawable/feedback_instructions_outline"
android:paddingHorizontal="@dimen/padding_default"
android:paddingVertical="0dp"
android:includeFontPadding="false"
android:gravity="center"
android:text="@string/face_capture_instructions_title"
android:textColor="@color/feedback_instructions_text"
app:backgroundTint="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

<androidx.camera.view.PreviewView
android:id="@+id/face_capture_camera"
android:layout_width="match_parent"
Expand Down
18 changes: 18 additions & 0 deletions face/capture/src/main/res/layout/fragment_live_feedback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,22 @@
app:layout_constraintTop_toBottomOf="@id/capture_feedback_txt_explanation"
tools:visibility="visible" />

<com.google.android.material.button.MaterialButton
android:id="@+id/capture_instructions_btn"
style="@style/Text.Subtitle1"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_margin="@dimen/margin_large"
android:background="@drawable/feedback_instructions_outline"
android:paddingHorizontal="@dimen/padding_default"
android:paddingVertical="0dp"
android:includeFontPadding="false"
android:gravity="center"
android:text="@string/face_capture_instructions_title"
android:textColor="@color/feedback_instructions_text"
app:backgroundTint="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,22 @@
app:layout_constraintTop_toBottomOf="@id/capture_feedback_txt_explanation"
tools:visibility="visible" />

<com.google.android.material.button.MaterialButton
android:id="@+id/capture_instructions_btn"
style="@style/Text.Subtitle1"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_margin="@dimen/margin_large"
android:background="@drawable/feedback_instructions_outline"
android:paddingHorizontal="@dimen/padding_default"
android:paddingVertical="0dp"
android:includeFontPadding="false"
android:gravity="center"
android:text="@string/face_capture_instructions_title"
android:textColor="@color/feedback_instructions_text"
app:backgroundTint="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
app:destination="@id/faceConfirmationFragment"
app:popUpTo="@+id/graph_face_capture_auto_internal"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_faceLiveFeedbackFragment_to_facePreparationFragment"
app:destination="@id/facePreparationFragment"
app:popUpTo="@+id/graph_face_capture_auto_internal"
app:popUpToInclusive="true" />
</fragment>

<fragment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
app:destination="@id/faceConfirmationFragment"
app:popUpTo="@+id/graph_face_capture_internal"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_faceLiveFeedbackFragment_to_facePreparationFragment"
app:destination="@id/facePreparationFragment"
app:popUpTo="@+id/graph_face_capture_internal"
app:popUpToInclusive="true" />
</fragment>

<fragment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.google.common.truth.Truth.assertThat
import com.simprints.core.tools.time.Timestamp
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
Expand Down Expand Up @@ -72,6 +73,9 @@ class FaceCaptureViewModelTest {
@MockK
private lateinit var isUsingAutoCapture: IsUsingAutoCaptureUseCase

@MockK
private lateinit var shouldShowInstructionsScreen: ShouldShowInstructionsScreenUseCase

private lateinit var viewModel: FaceCaptureViewModel

private val faceDetections = listOf<FaceDetection>(
Expand Down Expand Up @@ -100,6 +104,7 @@ class FaceCaptureViewModelTest {
},
saveLicenseCheckEvent,
isUsingAutoCapture,
shouldShowInstructionsScreen,
"deviceId",
)
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SharedPreferences.Editor>(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() }
}
}
1 change: 1 addition & 0 deletions infra/resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
<string name="face_capture_capturing_title">Live Capture</string>
<string name="face_capture_preparation_title">Preparation</string>
<string name="face_capture_start_capture">Start capture</string>
<string name="face_capture_instructions_title">Instructions</string>
<string name="face_capture_title_previewing">Preparing to scan</string>
<string name="face_capture_title_no_face">No face detected</string>
<string name="face_capture_error_no_face">Make sure the face fills the circle</string>
Expand Down