Skip to content

Instantly share code, notes, and snippets.

@lewisd1996
Created February 14, 2025 18:32
import android.content.Context
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.os.Build
import android.util.Log
import android.view.View
import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.Camera2Interop
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.ColorMatrixColorFilter
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.LocalLifecycleOwner
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@OptIn(ExperimentalCamera2Interop::class)
@Composable
fun CameraImageView(
modifier: Modifier = Modifier,
isGreyscale: Boolean = false,
cameraId: String?
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var showError by remember { mutableStateOf(false) }
val previewView = remember {
PreviewView(context).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
}
AndroidView(
modifier = modifier,
factory = { previewView },
update = { view ->
if (isGreyscale) applyGreyscale(view) else resetView(view)
}
)
LaunchedEffect(cameraId) {
if (cameraId.isNullOrEmpty()) return@LaunchedEffect
try {
val cameraProvider = ProcessCameraProvider.getInstance(context).get()
cameraProvider.unbindAll()
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
// Find if this is a physical camera and get its logical parent
val (isPhysical, logicalParentId) = findLogicalParent(cameraManager, cameraId)
val selectorId = logicalParentId ?: cameraId
// Build preview with Camera2Interop if physical
val preview = Preview.Builder().apply {
if (isPhysical) {
val characteristics = cameraManager.getCameraCharacteristics(selectorId)
if (characteristics.physicalCameraIds.contains(cameraId)) {
Camera2Interop.Extender(this)
.setPhysicalCameraId(cameraId)
.setStreamUseCase(CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW.toLong())
} else {
Log.e(
"CameraView",
"Requested physical camera ID $cameraId is not valid for logical camera $selectorId"
)
}
}
}.build().also { it.surfaceProvider = previewView.surfaceProvider }
// Use logical parent if available, otherwise original ID
val selector = CameraSelector.Builder()
.addCameraFilter {
cameraProvider.availableCameraInfos.filter {
Camera2CameraInfo.from(it).cameraId == selectorId
}
}
.build()
cameraProvider.bindToLifecycle(
lifecycleOwner,
selector,
preview
)
showError = false
} catch (e: Exception) {
Log.e("CameraView", "Binding failed: ${e.message}")
showError = true
}
}
if (showError) {
Box(modifier) {
Text("Camera unavailable", color = Color.Red)
}
}
}
private fun findLogicalParent(
cameraManager: CameraManager,
cameraId: String
): Pair<Boolean, String?> {
return try {
// Check if this camera is a physical sub-camera
val allCameraIds = cameraManager.cameraIdList
var logicalParent: String? = null
for (potentialLogicalId in allCameraIds) {
val chars = cameraManager.getCameraCharacteristics(potentialLogicalId)
if (chars.physicalCameraIds.contains(cameraId)) {
logicalParent = potentialLogicalId
break
}
}
// Return if physical and parent ID
(logicalParent != null) to logicalParent
} catch (e: Exception) {
false to null
}
}
private fun applyGreyscale(view: PreviewView) {
val paint = Paint().apply {
colorFilter = ColorMatrixColorFilter(ColorMatrix().apply { setToSaturation(0f) })
}
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint.asFrameworkPaint())
}
private fun resetView(view: PreviewView) {
view.setLayerType(View.LAYER_TYPE_NONE, null)
}
enum class CameraFacing {
FRONT, BACK, UNKNOWN
}
data class CameraOption(
val cameraId: String,
val lensType: LensType,
val description: String,
val facing: CameraFacing
)
@Singleton
class CameraRepository @Inject constructor(
private val context: Context
) {
private fun createCameraOption(
cameraId: String,
characteristics: CameraCharacteristics
): CameraOption {
return CameraOption(
cameraId = cameraId,
lensType = determineLensType(characteristics),
description = "Camera $cameraId",
facing = getFacingDirection(characteristics)
)
}
fun getAllCameraOptions(): List<CameraOption> {
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
return cameraManager.cameraIdList.flatMap { cameraId ->
try {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val cameras = mutableListOf<CameraOption>()
// Add logical camera
cameras.add(createCameraOption(cameraId, characteristics))
// Add physical cameras if available
if (isLogicalCamera(characteristics)) {
characteristics.physicalCameraIds.forEach { physicalId ->
try {
val physicalChars = cameraManager.getCameraCharacteristics(physicalId)
cameras.add(createCameraOption(physicalId, physicalChars))
} catch (e: Exception) {
Log.e("CameraRepo", "Error processing physical camera $physicalId", e)
}
}
}
cameras
} catch (e: Exception) {
Log.e("CameraRepo", "Error processing camera $cameraId", e)
emptyList()
}
}
}
private fun isLogicalCamera(characteristics: CameraCharacteristics): Boolean {
val capabilities = characteristics.get(
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES
) ?: return false
return capabilities.contains(
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
)
}
private fun determineLensType(characteristics: CameraCharacteristics): LensType {
val focalLengths =
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
val focalLength = focalLengths?.firstOrNull() ?: 0f
return when {
focalLength < 4.0f -> LensType.ULTRAWIDE
focalLength in 4.0f..6.0f -> LensType.WIDE
focalLength > 6.0f -> LensType.TELEPHOTO
else -> LensType.UNKNOWN
}
}
private fun getFacingDirection(characteristics: CameraCharacteristics): CameraFacing {
return when (characteristics.get(CameraCharacteristics.LENS_FACING)) {
CameraCharacteristics.LENS_FACING_FRONT -> CameraFacing.FRONT
CameraCharacteristics.LENS_FACING_BACK -> CameraFacing.BACK
else -> CameraFacing.UNKNOWN
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment