Avoid crash when camera is unavailable (#92)
This commit is contained in:
@@ -16,6 +16,7 @@ package org.fairscan.app.ui.screens.camera
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.Size
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.camera.core.CameraControl
|
import androidx.camera.core.CameraControl
|
||||||
@@ -31,17 +32,24 @@ import androidx.camera.core.resolutionselector.ResolutionStrategy
|
|||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import android.util.Size
|
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
@@ -49,15 +57,14 @@ import androidx.compose.ui.graphics.asImageBitmap
|
|||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.scale
|
import androidx.core.graphics.scale
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import org.fairscan.app.ui.components.CameraPermissionState
|
||||||
import org.fairscan.imageprocessing.Point
|
import org.fairscan.imageprocessing.Point
|
||||||
import org.fairscan.imageprocessing.scaledTo
|
import org.fairscan.imageprocessing.scaledTo
|
||||||
import org.fairscan.app.ui.components.CameraPermissionState
|
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@@ -68,6 +75,7 @@ fun CameraPreview(
|
|||||||
captureController: CameraCaptureController,
|
captureController: CameraCaptureController,
|
||||||
onPreviewViewReady: (PreviewView) -> Unit,
|
onPreviewViewReady: (PreviewView) -> Unit,
|
||||||
cameraPermission: CameraPermissionState,
|
cameraPermission: CameraPermissionState,
|
||||||
|
onError: (String, Throwable) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -79,44 +87,87 @@ fun CameraPreview(
|
|||||||
val cameraProviderFuture by remember {
|
val cameraProviderFuture by remember {
|
||||||
mutableStateOf(ProcessCameraProvider.getInstance(context))
|
mutableStateOf(ProcessCameraProvider.getInstance(context))
|
||||||
}
|
}
|
||||||
|
val analysisExecutor = remember { Executors.newSingleThreadExecutor() }
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner) {
|
DisposableEffect(lifecycleOwner) {
|
||||||
onDispose {
|
onDispose {
|
||||||
cameraProviderFuture.get().unbindAll()
|
cameraProviderFuture.get().unbindAll()
|
||||||
|
analysisExecutor.shutdown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AndroidView(modifier = modifier, factory = {
|
var bindState by remember { mutableStateOf<CameraBindState>(CameraBindState.Idle) }
|
||||||
val previewView = PreviewView(it).apply {
|
var retryKey by remember { mutableStateOf(0) }
|
||||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
var previewView: PreviewView? by remember { mutableStateOf(null) }
|
||||||
scaleType = PreviewView.ScaleType.FIT_CENTER
|
|
||||||
onPreviewViewReady(this)
|
when (bindState) {
|
||||||
}
|
is CameraBindState.Error -> {
|
||||||
val executor = Executors.newSingleThreadExecutor()
|
Column(
|
||||||
cameraProviderFuture.addListener({
|
modifier = Modifier.fillMaxSize(),
|
||||||
bindCameraUseCases(
|
verticalArrangement = Arrangement.Center,
|
||||||
lifecycleOwner = lifecycleOwner,
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
cameraProviderFuture = cameraProviderFuture,
|
) {
|
||||||
executor = executor,
|
Text("Camera unavailable")
|
||||||
previewView = previewView,
|
Spacer(Modifier.height(8.dp))
|
||||||
onImageAnalyzed = onImageAnalyzed,
|
Button(onClick = { retryKey++ }) {
|
||||||
captureController = captureController
|
Text("Retry")
|
||||||
)
|
}
|
||||||
}, ContextCompat.getMainExecutor(context))
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
AndroidView(
|
||||||
|
modifier = modifier,
|
||||||
|
factory = {
|
||||||
|
PreviewView(it).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
|
scaleType = PreviewView.ScaleType.FIT_CENTER
|
||||||
|
onPreviewViewReady(this)
|
||||||
|
previewView = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(previewView, retryKey) {
|
||||||
|
val view = previewView ?: return@LaunchedEffect
|
||||||
|
|
||||||
|
val provider = cameraProviderFuture.get()
|
||||||
|
|
||||||
|
val result = runCatching {
|
||||||
|
bindCameraUseCases(
|
||||||
|
lifecycleOwner,
|
||||||
|
provider,
|
||||||
|
analysisExecutor,
|
||||||
|
view,
|
||||||
|
onImageAnalyzed,
|
||||||
|
captureController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindState = result.fold(
|
||||||
|
onSuccess = { CameraBindState.Bound },
|
||||||
|
onFailure = {
|
||||||
|
onError("Camera unavailable", it)
|
||||||
|
CameraBindState.Error(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
previewView
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bindCameraUseCases(
|
fun bindCameraUseCases(
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
cameraProviderFuture: ListenableFuture<ProcessCameraProvider>,
|
cameraProvider: ProcessCameraProvider,
|
||||||
executor: ExecutorService,
|
executor: ExecutorService,
|
||||||
previewView: PreviewView,
|
previewView: PreviewView,
|
||||||
onImageAnalyzed: (ImageProxy) -> Unit,
|
onImageAnalyzed: (ImageProxy) -> Unit,
|
||||||
captureController: CameraCaptureController,
|
captureController: CameraCaptureController,
|
||||||
) {
|
) {
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
|
||||||
val ratio_4_3 = ResolutionSelector.Builder()
|
val ratio_4_3 = ResolutionSelector.Builder()
|
||||||
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY)
|
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY)
|
||||||
.build()
|
.build()
|
||||||
@@ -149,7 +200,6 @@ fun bindCameraUseCases(
|
|||||||
.build()
|
.build()
|
||||||
captureController.imageCapture = imageCapture
|
captureController.imageCapture = imageCapture
|
||||||
|
|
||||||
val cameraProvider = cameraProviderFuture.get()
|
|
||||||
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector,
|
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector,
|
||||||
imageAnalysis, preview, imageCapture)
|
imageAnalysis, preview, imageCapture)
|
||||||
captureController.cameraControl = camera.cameraControl
|
captureController.cameraControl = camera.cameraControl
|
||||||
@@ -236,3 +286,9 @@ class CameraCaptureController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface CameraBindState {
|
||||||
|
object Idle : CameraBindState
|
||||||
|
object Bound : CameraBindState
|
||||||
|
data class Error(val throwable: Throwable) : CameraBindState
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ fun CameraScreen(
|
|||||||
captureController = captureController,
|
captureController = captureController,
|
||||||
onPreviewViewReady = { view -> previewView = view },
|
onPreviewViewReady = { view -> previewView = view },
|
||||||
cameraPermission = cameraPermission,
|
cameraPermission = cameraPermission,
|
||||||
|
onError = { message, throwable -> cameraViewModel.logError(message, throwable) }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
pageListState =
|
pageListState =
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ sealed interface CameraEvent {
|
|||||||
class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
||||||
|
|
||||||
private val imageSegmentationService = appContainer.imageSegmentationService
|
private val imageSegmentationService = appContainer.imageSegmentationService
|
||||||
|
private val logger = appContainer.logger
|
||||||
|
|
||||||
private val _events = MutableSharedFlow<CameraEvent>()
|
private val _events = MutableSharedFlow<CameraEvent>()
|
||||||
val events = _events.asSharedFlow()
|
val events = _events.asSharedFlow()
|
||||||
@@ -156,6 +157,11 @@ class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
|||||||
_captureState.value = CaptureState.Idle
|
_captureState.value = CaptureState.Idle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logError(message:String, throwable: Throwable) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
logger.e("Camera", message, throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class CaptureState {
|
sealed class CaptureState {
|
||||||
@@ -203,4 +209,3 @@ fun toBitmap(bgr: Mat): Bitmap {
|
|||||||
rgba.release()
|
rgba.release()
|
||||||
return bmp
|
return bmp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user