diff --git a/app/build.gradle b/app/build.gradle index 9a683d1..53c45ea 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,7 +95,7 @@ dependencies { implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' - implementation "androidx.compose.material:material-icons-extended:1.4.3" + implementation "androidx.compose.material:material-icons-extended:1.5.0" implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.lifecycle:lifecycle-service:2.6.1' testImplementation 'junit:junit:4.13.2' @@ -106,7 +106,7 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' - implementation "androidx.navigation:navigation-compose:2.7.0-rc01" + implementation "androidx.navigation:navigation-compose:2.7.0" implementation 'com.google.dagger:hilt-android:2.46.1' annotationProcessor 'com.google.dagger:hilt-compiler:2.46.1' diff --git a/app/src/main/java/app/myzel394/alibi/CameraHandler.kt b/app/src/main/java/app/myzel394/alibi/CameraHandler.kt new file mode 100644 index 0000000..cbc3e41 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/CameraHandler.kt @@ -0,0 +1,170 @@ +package app.myzel394.alibi + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.ImageFormat +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CameraDevice.StateCallback.ERROR_CAMERA_DEVICE +import android.hardware.camera2.CameraDevice.StateCallback.ERROR_CAMERA_SERVICE +import android.hardware.camera2.CameraManager +import android.hardware.camera2.params.OutputConfiguration +import android.hardware.camera2.params.SessionConfiguration +import android.media.Image +import android.media.ImageReader +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.util.Log +import android.view.Surface +import androidx.annotation.RequiresApi +import androidx.core.content.ContentProviderCompat.requireContext +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getSystemService +import kotlinx.coroutines.suspendCancellableCoroutine +import java.io.File +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.Executor +import java.util.logging.Logger +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class CameraHandler( + private val manager: CameraManager, + private val device: CameraDevice, + private val handler: Handler, + private val thread: HandlerThread, +) { + private lateinit var imageReader: ImageReader + + @RequiresApi(Build.VERSION_CODES.P) + private suspend fun createCaptureSession( + outputs: List, + ): CameraCaptureSession = suspendCancellableCoroutine { cont -> + // I really don't want to use a deprecated method, but there is + // absolutely no documentation available for the new method. + device.createCaptureSession( + outputs, + object : CameraCaptureSession.StateCallback() { + override fun onConfigured(session: CameraCaptureSession) { + cont.resume(session) + } + + override fun onConfigureFailed(session: CameraCaptureSession) { + cont.resumeWithException(RuntimeException("Failed to configure session")) + } + }, + handler, + ) + } + + @RequiresApi(Build.VERSION_CODES.P) + public suspend fun takePhoto( + outputFile: File, + ) { + Log.d("Alibi", "Taking photo") + + Log.d("Alibi", "Creating Camera Characteristics") + val characteristics = manager.getCameraCharacteristics(CameraCharacteristics.LENS_FACING_BACK.toString()) + Log.d("Alibi", "Creating size") + val size = characteristics.get( + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! + .getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.height * it.width }!! + Log.d("Alibi", "Creating image reader") + imageReader = ImageReader.newInstance( + size.width, + size.height, + ImageFormat.JPEG, + 1, + ) + + imageReader.setOnImageAvailableListener({ reader -> + val image = reader.acquireLatestImage() + + // Save image to file + outputFile.outputStream().use { stream -> + image.planes[0].buffer.apply { + val bytes = ByteArray(remaining()) + get(bytes) + stream.write(bytes) + } + } + + println("Image saved to ${outputFile.absolutePath}") + + image.close() + }, handler) + + Log.d("Alibi", "Creating capture session") + val session = createCaptureSession(listOf(imageReader.surface)) + + Log.d("Alibi", "Creating capture request") + val captureRequest = device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) + captureRequest.addTarget(imageReader.surface) + + Log.d("Alibi", "Capturing") + session.capture(captureRequest.build(), null, handler) + Log.d("Alibi", "Success!") + } + + companion object { + const val IMAGE_BUFFER_SIZE = 3 + + fun getCameraManager( + context: Context, + ): CameraManager = getSystemService(context, CameraManager::class.java)!! + + fun createThread(): HandlerThread = HandlerThread("CameraHandler").apply { start() } + fun createHandler(thread: HandlerThread): Handler = Handler(thread.looper) + + @SuppressLint("MissingPermission") + suspend fun openCamera( + manager: CameraManager, + cameraId: String, + thread: HandlerThread, + ): CameraHandler = suspendCancellableCoroutine { cont -> + val handler = createHandler(thread) + manager.openCamera(cameraId, object : CameraDevice.StateCallback() { + override fun onOpened(device: CameraDevice) { + cont.resume( + CameraHandler( + manager, + device, + handler, + thread, + ) + ) + + } + + override fun onDisconnected(device: CameraDevice) { + Log.w("Alibi", "Camera $cameraId has been disconnected") + } + + override fun onError(device: CameraDevice, error: Int) { + val msg = when (error) { + ERROR_CAMERA_DEVICE -> "Fatal (device)" + ERROR_CAMERA_DISABLED -> "Device policy" + ERROR_CAMERA_IN_USE -> "Camera in use" + ERROR_CAMERA_SERVICE -> "Fatal (service)" + ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use" + else -> "Unknown" + } + val exc = RuntimeException("Camera $cameraId error: ($error) $msg") + Log.e("Alibi", exc.message, exc) + if (cont.isActive) cont.resumeWithException(exc) + } + }, handler) + } + + suspend fun openCamera( + context: Context, + ): CameraHandler { + val manager = getCameraManager(context) + val cameraId = manager.cameraIdList.first() + val thread = createThread() + return openCamera(manager, cameraId, thread) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/VideoRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/VideoRecorderModel.kt index 05e2c66..853a93a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/VideoRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/VideoRecorderModel.kt @@ -20,7 +20,6 @@ import app.myzel394.alibi.services.RecorderService import app.myzel394.alibi.services.VideoRecorderService @ExperimentalVideo class VideoRecorderModel: ViewModel() { - /* var recorderState by mutableStateOf(RecorderState.IDLE) private set var recordingTime by mutableStateOf(null) @@ -37,7 +36,7 @@ import app.myzel394.alibi.services.VideoRecorderService get() = recorderState === RecorderState.PAUSED val progress: Float - get() = (recordingTime!! / recorderService!!.settings!!.maxDuration).toFloat() + get() = 0f private var intent: Intent? = null var recorderService: VideoRecorderService? = null @@ -51,28 +50,6 @@ import app.myzel394.alibi.services.VideoRecorderService private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { - recorderService = ((service as RecorderService.RecorderBinder).getService() as VideoRecorderService).also { recorder -> - recorder.onStateChange = { state -> - recorderState = state - } - recorder.onRecordingTimeChange = { time -> - recordingTime = time - } - recorder.onAmplitudeChange = { amps -> - amplitudes = amps - onAmplitudeChange() - } - recorder.onError = { - recorderService!!.createLastRecording() - onError() - } - - recorderState = recorder.state - recordingTime = recorder.recordingTime - amplitudes = recorder.amplitudes - - recorder.startRecording() - } } override fun onServiceDisconnected(arg0: ComponentName) { @@ -99,7 +76,6 @@ import app.myzel394.alibi.services.VideoRecorderService fun stopRecording(context: Context, saveAsLastRecording: Boolean = true) { if (saveAsLastRecording) { - lastRecording = recorderService!!.createLastRecording() } runCatching { @@ -111,15 +87,12 @@ import app.myzel394.alibi.services.VideoRecorderService } fun pauseRecording() { - recorderService!!.changeState(RecorderState.PAUSED) } fun resumeRecording() { - recorderService!!.changeState(RecorderState.RECORDING) } fun setMaxAmplitudesAmount(amount: Int) { - recorderService?.amplitudesAmount = amount } @Composable @@ -130,6 +103,4 @@ import app.myzel394.alibi.services.VideoRecorderService } } } - - */ } diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt index 07a97f2..483ba99 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt @@ -1,13 +1,18 @@ package app.myzel394.alibi.ui.screens import android.annotation.SuppressLint +import android.content.Context import android.graphics.ImageFormat import android.hardware.camera2.CameraCaptureSession import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CameraManager import android.hardware.camera2.params.SessionConfiguration import android.media.Image import android.media.ImageReader +import android.os.Handler +import android.os.HandlerThread +import android.util.Log import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.camera.core.ImageCapture @@ -21,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -37,7 +43,9 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getSystemService import androidx.navigation.NavController +import app.myzel394.alibi.CameraHandler import app.myzel394.alibi.R import app.myzel394.alibi.ui.components.RecorderScreen.molecules.AudioRecordingStatus import app.myzel394.alibi.ui.components.RecorderScreen.molecules.StartRecording @@ -50,9 +58,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import androidx.compose.runtime.* import java.io.File -@SuppressLint("MissingPermission") +@SuppressLint("MissingPermission", "NewApi") @OptIn(ExperimentalMaterial3Api::class) @Composable fun RecorderScreen( @@ -64,63 +73,6 @@ fun RecorderScreen( val lifecycle = LocalLifecycleOwner.current val scope = rememberCoroutineScope() - val cameraController = remember {LifecycleCameraController(context)} - - LaunchedEffect(Unit) { - scope.launch { - // Take a picture - val manager = ContextCompat.getSystemService(context, android.hardware.camera2.CameraManager::class.java)!! - - val cameraId = manager.cameraIdList.first() - - manager.openCamera( - cameraId, - object: CameraDevice.StateCallback() { - override fun onDisconnected(p0: CameraDevice) { - } - - override fun onError(p0: CameraDevice, p1: Int) { - } - - override fun onOpened(p0: CameraDevice) { - val camDevice = p0 - - val cameraCharacteristics = manager.getCameraCharacteristics(cameraId) - val previewSize = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.getOutputSizes( - ImageFormat.JPEG).maxByOrNull { it.height * it.width }!! - val imageReader = ImageReader.newInstance(previewSize.width, previewSize.height, ImageFormat.JPEG, 1) - imageReader.setOnImageAvailableListener( - { reader -> - val image: Image = reader.acquireLatestImage() - }, - null - ) - - // Create capture - val captureBuilder = camDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) - captureBuilder.addTarget(imageReader.surface) - - // Create session - camDevice.createCaptureSession( - listOf(imageReader.surface), - object: CameraCaptureSession.StateCallback() { - override fun onConfigureFailed(p0: CameraCaptureSession) { - } - - override fun onConfigured(p0: CameraCaptureSession) { - val session = p0 - session.capture(captureBuilder.build(), object: CameraCaptureSession.CaptureCallback() {}, null) - } - }, - null - ) - } - }, - null - ) - } - } - Scaffold( topBar = { TopAppBar( @@ -141,6 +93,92 @@ fun RecorderScreen( } ) }, - ) {padding -> + ) { padding -> + val scope = rememberCoroutineScope() + + // Wait for `CameraHadler.openCamera` + var camera by remember { mutableStateOf(null) } + + Button( + modifier = Modifier + .padding(padding), + onClick = { + scope.launch { + if (camera == null) { + camera = CameraHandler.Companion.openCamera(context) + } + + camera!!.takePhoto( + File( + context.externalCacheDir!!.absolutePath, + "test.jpg" + ) + ) + } + }) { + Text("Take photo") + } + + /* + LaunchedEffect(Unit) { + val cameraManager = getSystemService(context, CameraManager::class.java)!! + val backgroundThread = HandlerThread("CameraVideo").apply { + start() + } + val backgroundHandler = Handler(backgroundThread.looper) + + val characteristics = cameraManager.getCameraCharacteristics(CameraCharacteristics.LENS_FACING_BACK.toString()) + + + val cameraStateCallback = object : CameraDevice.StateCallback() { + override fun onOpened(camera: CameraDevice) { + val captureStateCallback = object : CameraCaptureSession.StateCallback() { + override fun onConfigureFailed(session: CameraCaptureSession) { + Log.e("Camera", "Failed to configure camera") + } + override fun onConfigured(session: CameraCaptureSession) { + val onImageAvailableListener = object: ImageReader.OnImageAvailableListener{ + override fun onImageAvailable(reader: ImageReader) { + val image: Image = reader.acquireLatestImage() + } + } + val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) + + session.capture(captureRequest.build(), null, backgroundHandler) + } + } + + val previewSize = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.height * it.width }!! + val imageReader = ImageReader.newInstance(previewSize.width, previewSize.height, ImageFormat.JPEG, 1) + camera.createCaptureSession( + listOf(imageReader.surface), + captureStateCallback, + backgroundHandler + ) + } + + override fun onDisconnected(cameraDevice: CameraDevice) { + + } + + override fun onError(cameraDevice: CameraDevice, error: Int) { + val errorMsg = when(error) { + ERROR_CAMERA_DEVICE -> "Fatal (device)" + ERROR_CAMERA_DISABLED -> "Device policy" + ERROR_CAMERA_IN_USE -> "Camera in use" + ERROR_CAMERA_SERVICE -> "Fatal (service)" + ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use" + else -> "Unknown" + } + } + } + + cameraManager.openCamera( + CameraCharacteristics.LENS_FACING_BACK.toString(), + cameraStateCallback, + backgroundHandler + ) + } + */ } }