diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Photo.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Photo.kt index 389f16ad6c..eadcc07199 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Photo.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Photo.kt @@ -1,58 +1,132 @@ package com.mrousavy.camera.core import android.media.AudioManager -import android.util.Log import com.mrousavy.camera.core.extensions.takePicture import com.mrousavy.camera.core.types.Flash import com.mrousavy.camera.core.types.Orientation import com.mrousavy.camera.core.types.TakePhotoOptions import com.mrousavy.camera.core.utils.FileUtils +import androidx.camera.core.ImageCapture.OnImageCapturedCallback +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.ImageProxy +import android.os.Looper +import android.util.Log +import android.provider.MediaStore +import android.content.ContentValues +import java.io.File +import android.os.Environment +import java.io.FileOutputStream +import androidx.core.content.ContextCompat +import androidx.camera.core.ImageCapture.Metadata +import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability +import android.media.MediaActionSound +import androidx.exifinterface.media.ExifInterface + +fun isOnMainThread() = Looper.myLooper() == Looper.getMainLooper() +fun ensureBackgroundThread(callback: () -> Unit) { + if (isOnMainThread()) { + Thread { + callback() + }.start() + } else { + callback() + } +} + +val TAG = "CameraSession+Photo" + suspend fun CameraSession.takePhoto(options: TakePhotoOptions): Photo { val camera = camera ?: throw CameraNotReadyError() val configuration = configuration ?: throw CameraNotReadyError() val photoConfig = configuration.photo as? CameraConfiguration.Output.Enabled ?: throw PhotoNotEnabledError() val photoOutput = photoOutput ?: throw PhotoNotEnabledError() - // Log.i(CameraSession.TAG, "LP3 is zsl supported ${camera.cameraInfo.isZslSupported()}") - // This returns true - - /* - LP3: We can't have two PhotoOutput use cases bound, so this doesn't work - We also can't dynamically update the modes of the photoOutput using camerax - Currently, the added useFastMode option does nothing - if (options.useFastMode) { - Log.i(CameraSession.TAG,"LP3 Using fast photo mode") - photoOutput = photoOutputLockedFocus - } - */ - - // Flash if (options.flash != Flash.OFF && !camera.cameraInfo.hasFlashUnit()) { throw FlashUnavailableError() } photoOutput.flashMode = options.flash.toFlashMode() - // Shutter sound + val enableShutterSound = options.enableShutterSound && !audioManager.isSilent - // isMirrored (EXIF) + val shutterSound = if (enableShutterSound) MediaActionSound() else null + shutterSound?.load(MediaActionSound.SHUTTER_CLICK) + val isMirrored = photoConfig.config.isMirrored + val metadata = Metadata().apply { + isReversedHorizontal = isMirrored + } - // Shoot photo! - val photoFile = photoOutput.takePicture( - options.file.file, - isMirrored, - enableShutterSound, - metadataProvider, - callback, - CameraQueues.cameraExecutor - ) + var capturedAt = System.currentTimeMillis(); + val filename = "img_${capturedAt}.jpg" + photoOutput.takePicture(CameraQueues.cameraExecutor, object : OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + ensureBackgroundThread { + image.use { + if (enableShutterSound) { + shutterSound?.play(MediaActionSound.SHUTTER_CLICK) + } + + val directory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Light") + if (!directory.exists()) { + directory.mkdirs() + } + + // Create file with timestamp + val file = File(directory, filename) - // Parse resulting photo (EXIF data) - val size = FileUtils.getImageSize(photoFile.uri.path) - val rotation = photoOutput.targetRotation - val orientation = Orientation.fromSurfaceRotation(rotation) + try { + val buffer = image.planes[0].buffer + val bytes = ByteArray(buffer.remaining()).apply { + buffer.get(this) + } + FileOutputStream(file).use { output -> + output.write(bytes) + } - return Photo(photoFile.uri.path, size.width, size.height, orientation, isMirrored) + // Add the image to MediaStore so it appears in the gallery + val values = ContentValues().apply { + put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + put(MediaStore.Images.Media.DATE_ADDED, capturedAt / 1000) + put(MediaStore.Images.Media.DATE_TAKEN, capturedAt) + put(MediaStore.Images.Media.DATA, file.absolutePath) + } + + context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + Log.i(TAG, "Image saved successfully to: ${file.absolutePath}, height: ${image.height}, width: ${image.width}, format: ${image.format}") + + val exif = ExifInterface(file.absolutePath) + // Overwrite the original orientation if the quirk exists. + if (!ExifRotationAvailability().shouldUseExifOrientation(image)) { + exif.rotate(image.imageInfo.rotationDegrees) + } + if (metadata.isReversedHorizontal) { + exif.flipHorizontally() + } + if (metadata.isReversedVertical) { + exif.flipVertically() + } + exif.saveAttributes(); + Log.i(TAG, "EXIF data saved") + } catch (e: Exception) { + Log.e(TAG, "Error saving image: ${e.message}") + e.printStackTrace() + } + } + } + } + + override fun onError(exception: ImageCaptureException) { + Log.d(TAG, "onError: ${exception.message}") + } + }) + + return Photo( + "/storage/emulated/0/Pictures/Light/${filename}", + 0, + 0, + Orientation.fromSurfaceRotation(photoOutput.targetRotation), + isMirrored + ) } private val AudioManager.isSilent: Boolean