You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

196 lines
7.6 KiB

package example.imageviewer.view
import android.annotation.SuppressLint
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.OnImageCapturedCallback
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.gms.location.CurrentLocationRequest
import com.google.android.gms.location.LocationServices
import example.imageviewer.*
import example.imageviewer.icon.IconPhotoCamera
import example.imageviewer.model.GpsPosition
import example.imageviewer.model.PictureData
import example.imageviewer.model.createCameraPictureData
import imageviewer.shared.generated.resources.Res
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.nio.ByteBuffer
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.math.absoluteValue
private val executor = Executors.newSingleThreadExecutor()
@OptIn(ExperimentalPermissionsApi::class)
@Composable
actual fun CameraView(
modifier: Modifier,
onCapture: (picture: PictureData.Camera, image: PlatformStorableImage) -> Unit
) {
val cameraPermissionState = rememberMultiplePermissionsState(
listOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.ACCESS_COARSE_LOCATION,
android.Manifest.permission.ACCESS_FINE_LOCATION,
)
)
if (cameraPermissionState.allPermissionsGranted) {
CameraWithGrantedPermission(modifier, onCapture)
} else {
LaunchedEffect(Unit) {
cameraPermissionState.launchMultiplePermissionRequest()
}
}
}
@SuppressLint("MissingPermission")
@Composable
private fun CameraWithGrantedPermission(
modifier: Modifier,
onCapture: (picture: PictureData.Camera, image: PlatformStorableImage) -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val viewScope = rememberCoroutineScope()
var cameraProvider: ProcessCameraProvider? by remember { mutableStateOf(null) }
val preview = Preview.Builder().build()
val previewView = remember { PreviewView(context) }
val imageCapture: ImageCapture = remember { ImageCapture.Builder().build() }
var isFrontCamera by rememberSaveable { mutableStateOf(false) }
val cameraSelector = remember(isFrontCamera) {
val lensFacing =
if (isFrontCamera) {
CameraSelector.LENS_FACING_FRONT
} else {
CameraSelector.LENS_FACING_BACK
}
CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()
}
DisposableEffect(Unit) {
onDispose {
cameraProvider?.unbindAll()
}
}
LaunchedEffect(isFrontCamera) {
cameraProvider = suspendCoroutine<ProcessCameraProvider> { continuation ->
ProcessCameraProvider.getInstance(context).also { cameraProvider ->
cameraProvider.addListener({
continuation.resume(cameraProvider.get())
}, executor)
}
}
cameraProvider?.unbindAll()
cameraProvider?.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture
)
preview.setSurfaceProvider(previewView.surfaceProvider)
}
val nameAndDescription = createNewPhotoNameAndDescription()
var capturePhotoStarted by remember { mutableStateOf(false) }
Box(modifier = modifier.pointerInput(isFrontCamera) {
detectHorizontalDragGestures { change, dragAmount ->
if (dragAmount.absoluteValue > 50.0) {
isFrontCamera = !isFrontCamera
}
}
}) {
AndroidView({ previewView }, modifier = Modifier.fillMaxSize())
CircularButton(
imageVector = IconPhotoCamera,
modifier = Modifier.align(Alignment.BottomCenter).padding(36.dp),
enabled = !capturePhotoStarted,
) {
fun addLocationInfoAndReturnResult(imageBitmap: ImageBitmap) {
fun sendToStorage(gpsPosition: GpsPosition) {
onCapture(
createCameraPictureData(
name = nameAndDescription.name,
description = nameAndDescription.description,
gps = gpsPosition
),
AndroidStorableImage(imageBitmap)
)
capturePhotoStarted = false
}
LocationServices.getFusedLocationProviderClient(context)
.getCurrentLocation(CurrentLocationRequest.Builder().build(), null)
.apply {
addOnSuccessListener {
sendToStorage(GpsPosition(it.latitude, it.longitude))
}
addOnFailureListener {
sendToStorage(GpsPosition(0.0, 0.0))
}
}
}
capturePhotoStarted = true
imageCapture.takePicture(executor, object : OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val byteArray: ByteArray = image.planes[0].buffer.toByteArray()
val imageBitmap = byteArray.toImageBitmap()
image.close()
addLocationInfoAndReturnResult(imageBitmap)
}
})
viewScope.launch {
// TODO: There is a known issue with Android emulator
// https://partnerissuetracker.corp.google.com/issues/161034252
// After 5 seconds delay, let's assume that the bug appears and publish a prepared photo
delay(5000)
if (capturePhotoStarted) {
addLocationInfoAndReturnResult(
Res.readBytes("files/android-emulator-photo.jpg").toImageBitmap()
)
}
}
}
if (capturePhotoStarted) {
CircularProgressIndicator(
modifier = Modifier.size(80.dp).align(Alignment.Center),
color = Color.White.copy(alpha = 0.7f),
strokeWidth = 8.dp,
)
}
}
}
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
}