From 2381485509bd01ebf60866fa1b8c3cb897232ce9 Mon Sep 17 00:00:00 2001 From: Melad Raouf Date: Wed, 18 Mar 2026 18:42:47 +0000 Subject: [PATCH 1/3] Wait for capture overlay layout before camera setup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../livefeedback/LiveFeedbackFragment.kt | 76 ++++++++++--------- 1 file changed, 41 insertions(+), 35 deletions(-) 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 61f5fa1200..cdb8334ef6 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 @@ -17,6 +17,7 @@ import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.awaitInstance import androidx.core.content.ContextCompat import androidx.core.net.toUri +import androidx.core.view.doOnLayout import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -141,42 +142,47 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) } /** Initialize CameraX, and prepare to bind the camera use cases */ - private fun setUpCamera() = viewLifecycleOwner.lifecycleScope.launch { - if (::cameraExecutor.isInitialized && !cameraExecutor.isShutdown) { - return@launch - } - // 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) - } + private fun setUpCamera() = binding.captureOverlay.doOnLayout { + viewLifecycleOwner.lifecycleScope.launch { + if (::cameraExecutor.isInitialized && !cameraExecutor.isShutdown) { + return@launch + } + // 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@LiveFeedbackFragment, - DEFAULT_BACK_CAMERA, - preview, - imageAnalyzer, - ) - cameraControl = camera.cameraControl - // Attach the view's surface provider to preview use case - preview.surfaceProvider = binding.faceCaptureCamera.surfaceProvider - Simber.i("Camera setup finished", tag = FACE_CAPTURE) + 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@LiveFeedbackFragment, + DEFAULT_BACK_CAMERA, + preview, + imageAnalyzer, + ) + cameraControl = camera.cameraControl + // Attach the view's surface provider to preview use case + preview.surfaceProvider = binding.faceCaptureCamera.surfaceProvider + Simber.i("Camera setup finished", tag = FACE_CAPTURE) + } } override fun onResume() { From 0047a3f77d6a70e995242bbaed0d4fe848295d6a Mon Sep 17 00:00:00 2001 From: Melad Raouf Date: Wed, 18 Mar 2026 18:43:32 +0000 Subject: [PATCH 2/3] Replace deprecated CameraX target resolution API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../screens/livefeedback/LiveFeedbackFragment.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 cdb8334ef6..1eb43c1f9d 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 @@ -13,6 +13,8 @@ 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.core.resolutionselector.ResolutionSelector +import androidx.camera.core.resolutionselector.ResolutionStrategy import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.awaitInstance import androidx.core.content.ContextCompat @@ -154,10 +156,18 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) if (!::targetResolution.isInitialized) { targetResolution = Size(binding.captureOverlay.width, binding.captureOverlay.height) } + val resolutionSelector = ResolutionSelector + .Builder() + .setResolutionStrategy( + ResolutionStrategy( + targetResolution, + ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER, + ), + ).build() val imageAnalyzer = ImageAnalysis .Builder() - .setTargetResolution(targetResolution) + .setResolutionSelector(resolutionSelector) .setOutputImageRotationEnabled(true) .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888) .build() @@ -168,7 +178,7 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) // Preview val preview = Preview .Builder() - .setTargetResolution(targetResolution) + .setResolutionSelector(resolutionSelector) .build() val cameraProvider = ProcessCameraProvider.awaitInstance(requireContext()) cameraProvider.unbindAll() From 45cb2e629ff0fe142338ac8887443929ceb44130 Mon Sep 17 00:00:00 2001 From: Melad Raouf Date: Fri, 20 Mar 2026 08:52:08 +0000 Subject: [PATCH 3/3] Refactor camera setup to wait for layout completion before initialization --- .../livefeedback/LiveFeedbackFragment.kt | 98 +++++++++---------- .../simprints/infra/uibase/view/View.ext.kt | 38 ++++++- 2 files changed, 86 insertions(+), 50 deletions(-) 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 1eb43c1f9d..db9f7b8d5e 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 @@ -19,7 +19,6 @@ import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.awaitInstance import androidx.core.content.ContextCompat import androidx.core.net.toUri -import androidx.core.view.doOnLayout import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -40,6 +39,7 @@ import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ORCHESTRATION 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.awaitLayout import com.simprints.infra.uibase.view.setCheckedWithLeftDrawable import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint @@ -144,55 +144,55 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) } /** Initialize CameraX, and prepare to bind the camera use cases */ - private fun setUpCamera() = binding.captureOverlay.doOnLayout { - viewLifecycleOwner.lifecycleScope.launch { - if (::cameraExecutor.isInitialized && !cameraExecutor.isShutdown) { - return@launch - } - // 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 resolutionSelector = ResolutionSelector - .Builder() - .setResolutionStrategy( - ResolutionStrategy( - targetResolution, - ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER, - ), - ).build() - - val imageAnalyzer = ImageAnalysis - .Builder() - .setResolutionSelector(resolutionSelector) - .setOutputImageRotationEnabled(true) - .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888) - .build() - val cropAnalyzer = CropToTargetOverlayAnalyzer(binding.captureOverlay, ::analyze) - - imageAnalyzer.setAnalyzer(cameraExecutor, cropAnalyzer) - - // Preview - val preview = Preview - .Builder() - .setResolutionSelector(resolutionSelector) - .build() - val cameraProvider = ProcessCameraProvider.awaitInstance(requireContext()) - cameraProvider.unbindAll() - val camera = cameraProvider.bindToLifecycle( - this@LiveFeedbackFragment, - DEFAULT_BACK_CAMERA, - preview, - imageAnalyzer, - ) - cameraControl = camera.cameraControl - // Attach the view's surface provider to preview use case - preview.surfaceProvider = binding.faceCaptureCamera.surfaceProvider - Simber.i("Camera setup finished", tag = FACE_CAPTURE) + private fun setUpCamera() = viewLifecycleOwner.lifecycleScope.launch { + if (::cameraExecutor.isInitialized && !cameraExecutor.isShutdown) { + return@launch + } + // Wait for the views to be properly laid out + binding.captureOverlay.awaitLayout() + // 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 resolutionSelector = ResolutionSelector + .Builder() + .setResolutionStrategy( + ResolutionStrategy( + targetResolution, + ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER, + ), + ).build() + + val imageAnalyzer = ImageAnalysis + .Builder() + .setResolutionSelector(resolutionSelector) + .setOutputImageRotationEnabled(true) + .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888) + .build() + val cropAnalyzer = CropToTargetOverlayAnalyzer(binding.captureOverlay, ::analyze) + + imageAnalyzer.setAnalyzer(cameraExecutor, cropAnalyzer) + + // Preview + val preview = Preview + .Builder() + .setResolutionSelector(resolutionSelector) + .build() + val cameraProvider = ProcessCameraProvider.awaitInstance(requireContext()) + cameraProvider.unbindAll() + val camera = cameraProvider.bindToLifecycle( + this@LiveFeedbackFragment, + DEFAULT_BACK_CAMERA, + preview, + imageAnalyzer, + ) + cameraControl = camera.cameraControl + // Attach the view's surface provider to preview use case + preview.surfaceProvider = binding.faceCaptureCamera.surfaceProvider + Simber.i("Camera setup finished", tag = FACE_CAPTURE) } override fun onResume() { diff --git a/infra/ui-base/src/main/java/com/simprints/infra/uibase/view/View.ext.kt b/infra/ui-base/src/main/java/com/simprints/infra/uibase/view/View.ext.kt index 444027fd3d..abcce566ed 100644 --- a/infra/ui-base/src/main/java/com/simprints/infra/uibase/view/View.ext.kt +++ b/infra/ui-base/src/main/java/com/simprints/infra/uibase/view/View.ext.kt @@ -2,13 +2,14 @@ package com.simprints.infra.uibase.view import android.animation.ObjectAnimator import android.view.View -import android.view.ViewPropertyAnimator import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateInterpolator import android.view.animation.DecelerateInterpolator import androidx.core.view.isVisible import androidx.fragment.app.Fragment import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume @ExcludedFromGeneratedTestCoverageReports("View animation") fun View.setPulseAnimation(isEnabled: Boolean) { @@ -75,3 +76,38 @@ private const val PULSE_ANIMATION_ALPHA_FULL = 1.0f private const val PULSE_ANIMATION_ALPHA_INTERMEDIATE = 0.9f private const val PULSE_ANIMATION_ALPHA_MIN = 0.6f private const val PULSE_ANIMATION_DURATION_MILLIS = 2000L + +@ExcludedFromGeneratedTestCoverageReports("View extension") +suspend fun View.awaitLayout() = suspendCancellableCoroutine { continuation -> + if (isLaidOut) { + continuation.resume(Unit) + return@suspendCancellableCoroutine + } + + val listener = object : View.OnLayoutChangeListener { + @ExcludedFromGeneratedTestCoverageReports("View extension") + override fun onLayoutChange( + v: View, + l: Int, + t: Int, + r: Int, + b: Int, + ol: Int, + ot: Int, + or: Int, + ob: Int, + ) { + v.removeOnLayoutChangeListener(this) + + if (continuation.isActive) { + continuation.resume(Unit) + } + } + } + + addOnLayoutChangeListener(listener) + + continuation.invokeOnCancellation { + removeOnLayoutChangeListener(listener) + } +}