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 @@ -26,6 +26,7 @@ import com.simprints.infra.logging.Simber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject

@HiltViewModel
Expand All @@ -43,6 +44,8 @@ internal class FaceCaptureViewModel @Inject constructor(
var attemptNumber: Int = 0
var samplesToCapture = 1

var shouldCheckCameraPermissions = AtomicBoolean(true)

private var faceDetections = listOf<FaceDetection>()

val recaptureEvent: LiveData<LiveDataEvent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import javax.inject.Inject


internal class FrameProcessor @Inject constructor(
private val imageProxyToBitmap: ImageProxyToBitmapUseCase
private val imageProxyToBitmap: ImageProxyToBitmapUseCase,
) {

private var previewViewWidth: Int = 0
private var previewViewHeight: Int = 0

private lateinit var boxOnTheScreen: RectF
private lateinit var cropRect: Rect
private var cropRect: Rect? = null

/**
* Init the frame processor
Expand All @@ -42,15 +42,14 @@ internal class FrameProcessor @Inject constructor(
* @param image
* @return Bitmap
*/
fun cropRotateFrame(image: ImageProxy): Bitmap {
if (!this::cropRect.isInitialized) {
// The cropRect should be calculated once as its value will be the same for all images.
calcRotatedCropRect(image)
}
return imageProxyToBitmap(image, cropRect)
fun cropRotateFrame(image: ImageProxy): Bitmap? {
val rect = cropRect?.takeUnless { it.isEmpty }
?: calcRotatedCropRect(image).also { cropRect = it }

return imageProxyToBitmap(image, rect)
}

private fun calcRotatedCropRect(image: ImageProxy) {
private fun calcRotatedCropRect(image: ImageProxy): Rect {
val cameraWidth = image.width
val cameraHeight = image.height

Expand All @@ -67,7 +66,7 @@ internal class FrameProcessor @Inject constructor(
val newBoundingBox =
CameraTargetOverlay.rectForPlane(rotatedCameraWidth, rotatedCameraHeight, newRectSize)

cropRect = getRotatedBoundingBox(
return getRotatedBoundingBox(
image.imageInfo.rotationDegrees,
newBoundingBox,
cameraWidth,
Expand All @@ -79,7 +78,7 @@ internal class FrameProcessor @Inject constructor(
rotation: Int,
newBoundingBox: RectF,
cameraWidth: Int,
cameraHeight: Int
cameraHeight: Int,
): RectF {
return when (360 - rotation) {
0, 360 -> newBoundingBox
Expand Down Expand Up @@ -113,7 +112,7 @@ internal class FrameProcessor @Inject constructor(
screenHeight: Int,
cameraWidth: Int,
cameraHeight: Int,
boxOnTheScreen: RectF
boxOnTheScreen: RectF,
): Float {
return if (screenWidth == cameraWidth || screenHeight == cameraHeight) {
val cameraArea = cameraHeight * cameraWidth
Expand Down Expand Up @@ -157,7 +156,7 @@ internal class FrameProcessor @Inject constructor(
screenWidth: Int,
cameraHeight: Int,
screenHeight: Int,
currentWidth: Float
currentWidth: Float,
): Float {
val widthRatio = cameraWidth / screenWidth.toFloat()
val heightRatio = cameraHeight / screenHeight.toFloat()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.simprints.face.capture.screens.livefeedback

import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.util.Size
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.ImageAnalysis
Expand All @@ -13,14 +15,18 @@ import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isInvisible
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 androidx.work.await
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.models.FaceDetection
Expand Down Expand Up @@ -55,30 +61,24 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

private lateinit var screenSize: Size


private val launchPermissionRequest = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { granted ->
if (!granted) {
Simber.i("Camera Permission not granted")
Toast.makeText(
requireContext(),
IDR.string.face_capturing_permission_denied,
Toast.LENGTH_LONG
).show()
} else {
setUpCamera()
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)
initFragment()
}

private fun initFragment() {
screenSize = with(resources.displayMetrics) { Size(widthPixels, widthPixels) }

bindViewModel()

binding.captureFeedbackTxtTitle.setOnClickListener { vm.startCapture() }
Expand Down Expand Up @@ -119,24 +119,28 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)
preview.setSurfaceProvider(binding.faceCaptureCamera.surfaceProvider)
}

override fun onStart() {
super.onStart()
override fun onResume() {
super.onResume()

// Check permission in onStart() 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()
if (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 {
launchPermissionRequest.launch(Manifest.permission.CAMERA)
mainVm.shouldCheckCameraPermissions.set(true)
}
}

override fun onStop() {
override fun onPause() {
// Shut down our background executor
if(::cameraExecutor.isInitialized) {
if (::cameraExecutor.isInitialized) {
cameraExecutor.shutdown()
}
super.onStop()
super.onPause()
}

private fun bindViewModel() {
Expand All @@ -152,7 +156,10 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

LiveFeedbackFragmentViewModel.CapturingState.FINISHED -> {
mainVm.captureFinished(vm.sortedQualifyingCaptures)
findNavController().navigateSafely(this, R.id.action_faceLiveFeedbackFragment_to_faceConfirmationFragment)
findNavController().navigateSafely(
this,
R.id.action_faceLiveFeedbackFragment_to_faceConfirmationFragment
)
}

}
Expand Down Expand Up @@ -199,8 +206,10 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)
private fun renderCapturingNotStarted() {
binding.apply {
captureOverlay.drawSemiTransparentTarget()
captureTitle.text = getString(IDR.string.face_capture_preparation_title)
captureFeedbackTxtTitle.text = getString(IDR.string.face_capture_title_previewing)
captureTitle.setText(IDR.string.face_capture_preparation_title)
captureFeedbackTxtTitle.isVisible = true
captureFeedbackTxtTitle.setText(IDR.string.face_capture_title_previewing)
captureFeedbackPermissionButton.isGone = true
}
toggleCaptureButtons(false)
}
Expand All @@ -209,17 +218,20 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)
renderCapturingStateColors()
binding.apply {
captureProgress.isVisible = true
captureTitle.text = getString(IDR.string.face_capture_capturing_title)
captureFeedbackTxtTitle.text =
getString(IDR.string.face_capture_prep_begin_button_capturing)
captureTitle.setText(IDR.string.face_capture_capturing_title)
captureFeedbackTxtTitle.isVisible = true
captureFeedbackTxtTitle.setText(IDR.string.face_capture_prep_begin_button_capturing)
captureFeedbackPermissionButton.isGone = true
}
toggleCaptureButtons(false)
}

private fun renderValidFace() {
binding.apply {
captureFeedbackTxtTitle.text = getString(IDR.string.face_capture_begin_button)
captureFeedbackTxtTitle.isVisible = true
captureFeedbackTxtTitle.setText(IDR.string.face_capture_begin_button)
captureFeedbackTxtExplanation.text = null
captureFeedbackPermissionButton.isGone = true

captureFeedbackTxtTitle.setCheckedWithLeftDrawable(
true, ContextCompat.getDrawable(requireContext(), R.drawable.ic_checked_white_18dp)
Expand All @@ -230,9 +242,10 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

private fun renderValidCapturingFace() {
binding.apply {
captureFeedbackTxtTitle.text =
getString(IDR.string.face_capture_prep_begin_button_capturing)
captureFeedbackTxtExplanation.text = getString(IDR.string.face_capture_hold)
captureFeedbackTxtTitle.isVisible = true
captureFeedbackTxtTitle.setText(IDR.string.face_capture_prep_begin_button_capturing)
captureFeedbackTxtExplanation.setText(IDR.string.face_capture_hold)
captureFeedbackPermissionButton.isGone = true

captureFeedbackTxtTitle.setCheckedWithLeftDrawable(
true, ContextCompat.getDrawable(requireContext(), R.drawable.ic_checked_white_18dp)
Expand All @@ -244,8 +257,10 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

private fun renderFaceTooFar() {
binding.apply {
captureFeedbackTxtTitle.text = getString(IDR.string.face_capture_title_too_far)
captureFeedbackTxtExplanation.text = getString(IDR.string.face_capture_error_too_far)
captureFeedbackTxtTitle.isVisible = true
captureFeedbackTxtTitle.setText(IDR.string.face_capture_title_too_far)
captureFeedbackTxtExplanation.setText(IDR.string.face_capture_error_too_far)
captureFeedbackPermissionButton.isGone = true

captureFeedbackTxtTitle.setCheckedWithLeftDrawable(false)
}
Expand All @@ -256,8 +271,10 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

private fun renderFaceTooClose() {
binding.apply {
captureFeedbackTxtTitle.text = getString(IDR.string.face_capture_title_too_close)
captureFeedbackTxtExplanation.text = getString(IDR.string.face_capture_error_too_close)
captureFeedbackTxtTitle.isVisible = true
captureFeedbackTxtTitle.setText(IDR.string.face_capture_title_too_close)
captureFeedbackTxtExplanation.setText(IDR.string.face_capture_error_too_close)
captureFeedbackPermissionButton.isInvisible = true

captureFeedbackTxtTitle.setCheckedWithLeftDrawable(false)
}
Expand All @@ -268,8 +285,10 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

private fun renderNoFace() {
binding.apply {
captureFeedbackTxtTitle.text = getString(IDR.string.face_capture_title_no_face)
captureFeedbackTxtExplanation.text = getString(IDR.string.face_capture_error_no_face)
captureFeedbackTxtTitle.isVisible = true
captureFeedbackTxtTitle.setText(IDR.string.face_capture_title_no_face)
captureFeedbackTxtExplanation.setText(IDR.string.face_capture_error_no_face)
captureFeedbackPermissionButton.isGone = true

captureFeedbackTxtTitle.setCheckedWithLeftDrawable(false)
}
Expand All @@ -280,9 +299,10 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

private fun renderFaceNotStraight() {
binding.apply {
captureFeedbackTxtTitle.text = getString(IDR.string.face_capture_title_look_straight)
captureFeedbackTxtExplanation.text =
getString(IDR.string.face_capture_error_look_straight)
captureFeedbackTxtTitle.isVisible = true
captureFeedbackTxtTitle.setText(IDR.string.face_capture_title_look_straight)
captureFeedbackTxtExplanation.setText(IDR.string.face_capture_error_look_straight)
captureFeedbackPermissionButton.isGone = true

captureFeedbackTxtTitle.setCheckedWithLeftDrawable(false)
}
Expand All @@ -307,6 +327,30 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)
private fun toggleCaptureButtons(valid: Boolean) {
binding.captureFeedbackTxtTitle.isClickable = valid
}

private fun renderNoPermission(shouldOpenSettings: Boolean) {
binding.apply {
captureOverlay.drawSemiTransparentTarget()
captureFeedbackTxtTitle.isInvisible = true
captureFeedbackTxtExplanation.setText(IDR.string.face_capture_permission_denied)

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)
}
}
}
toggleCaptureButtons(false)
}

}


Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor(
fun process(image: ImageProxy) {
val captureStartTime = timeHelper.now()
val croppedBitmap = frameProcessor.cropRotateFrame(image)

if (croppedBitmap == null) {
image.close()
return
}
val potentialFace = faceDetector.analyze(croppedBitmap)

val faceDetection = getFaceDetectionFromPotentialFace(croppedBitmap, potentialFace)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import javax.inject.Inject
*/
internal class ImageProxyToBitmapUseCase @Inject constructor() {

operator fun invoke(imageProxy: ImageProxy, cropRect: Rect): Bitmap {
operator fun invoke(imageProxy: ImageProxy, cropRect: Rect): Bitmap? {
require(imageProxy.format == PixelFormat.RGBA_8888) {
"${imageProxy.format} is not supported. RGBA_8888 is the only supported image format"
}
Expand All @@ -27,6 +27,11 @@ internal class ImageProxyToBitmapUseCase @Inject constructor() {
bitmap.copyPixelsFromBuffer(buffer)
val rotationMatrix = Matrix()
rotationMatrix.postRotate(imageProxy.imageInfo.rotationDegrees.toFloat())

if (cropRect.isEmpty) {
return null
}

val croppedRotatedBitmap = Bitmap.createBitmap(
bitmap,
cropRect.left,
Expand Down
Loading