Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<CameraConfiguration.Photo> ?: 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
Expand Down