|
1 | 1 | package com.mrousavy.camera.core |
2 | 2 |
|
3 | 3 | import android.media.AudioManager |
4 | | -import android.util.Log |
5 | 4 | import com.mrousavy.camera.core.extensions.takePicture |
6 | 5 | import com.mrousavy.camera.core.types.Flash |
7 | 6 | import com.mrousavy.camera.core.types.Orientation |
8 | 7 | import com.mrousavy.camera.core.types.TakePhotoOptions |
9 | 8 | import com.mrousavy.camera.core.utils.FileUtils |
10 | 9 |
|
| 10 | +import androidx.camera.core.ImageCapture.OnImageCapturedCallback |
| 11 | +import androidx.camera.core.ImageCaptureException |
| 12 | +import androidx.camera.core.ImageProxy |
| 13 | +import android.os.Looper |
| 14 | +import android.util.Log |
| 15 | +import android.provider.MediaStore |
| 16 | +import android.content.ContentValues |
| 17 | +import java.io.File |
| 18 | +import android.os.Environment |
| 19 | +import java.io.FileOutputStream |
| 20 | +import androidx.core.content.ContextCompat |
| 21 | +import androidx.camera.core.ImageCapture.Metadata |
| 22 | +import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability |
| 23 | +import android.media.MediaActionSound |
| 24 | +import androidx.exifinterface.media.ExifInterface |
| 25 | + |
| 26 | +fun isOnMainThread() = Looper.myLooper() == Looper.getMainLooper() |
| 27 | +fun ensureBackgroundThread(callback: () -> Unit) { |
| 28 | + if (isOnMainThread()) { |
| 29 | + Thread { |
| 30 | + callback() |
| 31 | + }.start() |
| 32 | + } else { |
| 33 | + callback() |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +val TAG = "CameraSession+Photo" |
| 38 | + |
11 | 39 | suspend fun CameraSession.takePhoto(options: TakePhotoOptions): Photo { |
12 | 40 | val camera = camera ?: throw CameraNotReadyError() |
13 | 41 | val configuration = configuration ?: throw CameraNotReadyError() |
14 | 42 | val photoConfig = configuration.photo as? CameraConfiguration.Output.Enabled<CameraConfiguration.Photo> ?: throw PhotoNotEnabledError() |
15 | 43 | val photoOutput = photoOutput ?: throw PhotoNotEnabledError() |
16 | 44 |
|
17 | | - // Log.i(CameraSession.TAG, "LP3 is zsl supported ${camera.cameraInfo.isZslSupported()}") |
18 | | - // This returns true |
19 | | - |
20 | | - /* |
21 | | - LP3: We can't have two PhotoOutput use cases bound, so this doesn't work |
22 | | - We also can't dynamically update the modes of the photoOutput using camerax |
23 | | - Currently, the added useFastMode option does nothing |
24 | | - if (options.useFastMode) { |
25 | | - Log.i(CameraSession.TAG,"LP3 Using fast photo mode") |
26 | | - photoOutput = photoOutputLockedFocus |
27 | | - } |
28 | | - */ |
29 | | - |
30 | | - // Flash |
31 | 45 | if (options.flash != Flash.OFF && !camera.cameraInfo.hasFlashUnit()) { |
32 | 46 | throw FlashUnavailableError() |
33 | 47 | } |
34 | 48 | photoOutput.flashMode = options.flash.toFlashMode() |
35 | | - // Shutter sound |
| 49 | + |
36 | 50 | val enableShutterSound = options.enableShutterSound && !audioManager.isSilent |
37 | | - // isMirrored (EXIF) |
| 51 | + val shutterSound = if (enableShutterSound) MediaActionSound() else null |
| 52 | + shutterSound?.load(MediaActionSound.SHUTTER_CLICK) |
| 53 | + |
38 | 54 | val isMirrored = photoConfig.config.isMirrored |
| 55 | + val metadata = Metadata().apply { |
| 56 | + isReversedHorizontal = isMirrored |
| 57 | + } |
39 | 58 |
|
40 | | - // Shoot photo! |
41 | | - val photoFile = photoOutput.takePicture( |
42 | | - options.file.file, |
43 | | - isMirrored, |
44 | | - enableShutterSound, |
45 | | - metadataProvider, |
46 | | - callback, |
47 | | - CameraQueues.cameraExecutor |
48 | | - ) |
| 59 | + var capturedAt = System.currentTimeMillis(); |
| 60 | + val filename = "img_${capturedAt}.jpg" |
| 61 | + photoOutput.takePicture(CameraQueues.cameraExecutor, object : OnImageCapturedCallback() { |
| 62 | + override fun onCaptureSuccess(image: ImageProxy) { |
| 63 | + ensureBackgroundThread { |
| 64 | + if (enableShutterSound) { |
| 65 | + shutterSound?.play(MediaActionSound.SHUTTER_CLICK) |
| 66 | + } |
| 67 | + |
| 68 | + val directory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Light") |
| 69 | + if (!directory.exists()) { |
| 70 | + directory.mkdirs() |
| 71 | + } |
| 72 | + |
| 73 | + // Create file with timestamp |
| 74 | + val file = File(directory, filename) |
49 | 75 |
|
50 | | - // Parse resulting photo (EXIF data) |
51 | | - val size = FileUtils.getImageSize(photoFile.uri.path) |
52 | | - val rotation = photoOutput.targetRotation |
53 | | - val orientation = Orientation.fromSurfaceRotation(rotation) |
| 76 | + try { |
| 77 | + val buffer = image.planes[0].buffer |
| 78 | + val bytes = ByteArray(buffer.remaining()).apply { |
| 79 | + buffer.get(this) |
| 80 | + } |
| 81 | + FileOutputStream(file).use { output -> |
| 82 | + output.write(bytes) |
| 83 | + } |
54 | 84 |
|
55 | | - return Photo(photoFile.uri.path, size.width, size.height, orientation, isMirrored) |
| 85 | + // Add the image to MediaStore so it appears in the gallery |
| 86 | + val values = ContentValues().apply { |
| 87 | + put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") |
| 88 | + put(MediaStore.Images.Media.DATE_ADDED, capturedAt / 1000) |
| 89 | + put(MediaStore.Images.Media.DATE_TAKEN, capturedAt) |
| 90 | + put(MediaStore.Images.Media.DATA, file.absolutePath) |
| 91 | + } |
| 92 | + |
| 93 | + context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) |
| 94 | + Log.i(TAG, "Image saved successfully to: ${file.absolutePath}, height: ${image.height}, width: ${image.width}, format: ${image.format}") |
| 95 | + |
| 96 | + val exif = ExifInterface(file.absolutePath) |
| 97 | + // Overwrite the original orientation if the quirk exists. |
| 98 | + if (!ExifRotationAvailability().shouldUseExifOrientation(image)) { |
| 99 | + exif.rotate(image.imageInfo.rotationDegrees) |
| 100 | + } |
| 101 | + if (metadata.isReversedHorizontal) { |
| 102 | + exif.flipHorizontally() |
| 103 | + } |
| 104 | + if (metadata.isReversedVertical) { |
| 105 | + exif.flipVertically() |
| 106 | + } |
| 107 | + exif.saveAttributes(); |
| 108 | + Log.i(TAG, "EXIF data saved") |
| 109 | + } catch (e: Exception) { |
| 110 | + Log.e(TAG, "Error saving image: ${e.message}") |
| 111 | + e.printStackTrace() |
| 112 | + } |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + override fun onError(exception: ImageCaptureException) { |
| 117 | + Log.d(TAG, "onError: ${exception.message}") |
| 118 | + } |
| 119 | + }) |
| 120 | + |
| 121 | + return Photo( |
| 122 | + "/storage/emulated/0/Pictures/Light/${filename}", |
| 123 | + 0, |
| 124 | + 0, |
| 125 | + Orientation.fromSurfaceRotation(photoOutput.targetRotation), |
| 126 | + isMirrored |
| 127 | + ) |
56 | 128 | } |
57 | 129 |
|
58 | 130 | private val AudioManager.isSilent: Boolean |
|
0 commit comments