Files
Simple-Camera/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaOutputHelper.kt
darthpaul cc3c2108cf properly play media sound on image capture
- instead of using the OnImageSavedCallback, we now use the OnImageCaptureCallback and we play the sound in the onCapturedSuccess method to prevent the user from hearing the capture sound before the actual capture takes place
- add ImageSaver and ImageUtil based on CameraX implementation
- add sealed interfaces ImageCaptureOutput and VideoCaptureOutput to prevent confusion on the supported output format for images and video
- ImageSaver supports saving images with all MediaOutput.ImageCaptureOutput that can be saved to disk; MediaStoreOutput, OutputStreamMediaOutput and FileMediaOutput (unused at the moment), it throws an UnsupportedOperation when trying to save a BitmapOutput.
- with ImageSaver, we now have control of whether the Exif attributes are written to disk and so we can get rid of ExifRemover
- delete the extension method ImageProxy.toJpegByteArray as we have a similar method in ImageUtils class
2022-10-08 03:07:42 +01:00

233 lines
9.2 KiB
Kotlin

package com.simplemobiletools.camera.helpers
import android.content.ContentValues
import android.net.Uri
import android.os.Environment
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import com.simplemobiletools.camera.extensions.config
import com.simplemobiletools.camera.extensions.getOutputMediaFile
import com.simplemobiletools.camera.extensions.getRandomMediaName
import com.simplemobiletools.camera.models.MediaOutput
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isOreoPlus
import com.simplemobiletools.commons.helpers.isQPlus
import java.io.File
import java.io.OutputStream
class MediaOutputHelper(
private val activity: BaseSimpleActivity,
private val errorHandler: CameraErrorHandler,
private val outputUri: Uri?,
private val is3rdPartyIntent: Boolean,
) {
companion object {
private const val MODE = "rw"
private const val EXTERNAL_VOLUME = "external"
private const val IMAGE_MIME_TYPE = "image/jpeg"
private const val VIDEO_MIME_TYPE = "video/mp4"
}
private val mediaStorageDir = activity.config.savePhotosFolder
private val contentResolver = activity.contentResolver
fun getImageMediaOutput(): MediaOutput.ImageCaptureOutput {
return try {
if (is3rdPartyIntent) {
if (outputUri != null) {
val outputStream = openOutputStream(outputUri)
if (outputStream != null) {
MediaOutput.OutputStreamMediaOutput(outputStream, outputUri)
} else {
errorHandler.showSaveToInternalStorage()
getMediaStoreOutput(isPhoto = true)
}
} else {
MediaOutput.BitmapOutput
}
} else {
getOutputStreamMediaOutput() ?: getMediaStoreOutput(isPhoto = true)
}
} catch (e: Exception) {
errorHandler.showSaveToInternalStorage()
getMediaStoreOutput(isPhoto = true)
}
}
fun getVideoMediaOutput(): MediaOutput.VideoCaptureOutput {
return try {
if (is3rdPartyIntent) {
if (outputUri != null) {
if (isOreoPlus()) {
val fileDescriptor = openFileDescriptor(outputUri)
if (fileDescriptor != null) {
MediaOutput.FileDescriptorMediaOutput(fileDescriptor, outputUri)
} else {
errorHandler.showSaveToInternalStorage()
getMediaStoreOutput(isPhoto = false)
}
} else {
val path = activity.getRealPathFromURI(outputUri)
if (path != null) {
MediaOutput.FileMediaOutput(File(path), outputUri)
} else {
errorHandler.showSaveToInternalStorage()
getMediaStoreOutput(isPhoto = false)
}
}
} else {
getMediaStoreOutput(isPhoto = false)
}
} else {
if (isOreoPlus()) {
getFileDescriptorMediaOutput() ?: getMediaStoreOutput(isPhoto = false)
} else {
getFileMediaOutput() ?: getMediaStoreOutput(isPhoto = false)
}
}
} catch (e: Exception) {
errorHandler.showSaveToInternalStorage()
getMediaStoreOutput(isPhoto = false)
}
}
private fun getMediaStoreOutput(isPhoto: Boolean): MediaOutput.MediaStoreOutput {
val contentValues = getContentValues(isPhoto)
val contentUri = if (isPhoto) {
MediaStore.Images.Media.getContentUri(EXTERNAL_VOLUME)
} else {
MediaStore.Video.Media.getContentUri(EXTERNAL_VOLUME)
}
return MediaOutput.MediaStoreOutput(contentValues, contentUri)
}
@Suppress("DEPRECATION")
private fun getContentValues(isPhoto: Boolean): ContentValues {
val mimeType = if (isPhoto) IMAGE_MIME_TYPE else VIDEO_MIME_TYPE
return ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(isPhoto))
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
if (isQPlus()) {
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
} else {
put(MediaStore.MediaColumns.DATA, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString())
}
}
}
private fun getOutputStreamMediaOutput(): MediaOutput.OutputStreamMediaOutput? {
var mediaOutput: MediaOutput.OutputStreamMediaOutput? = null
val canWrite = canWriteToFilePath(mediaStorageDir)
if (canWrite) {
val path = activity.getOutputMediaFile(true)
val uri = getUriForFilePath(path)
val outputStream = activity.getFileOutputStreamSync(path, path.getMimeType())
if (uri != null && outputStream != null) {
mediaOutput = MediaOutput.OutputStreamMediaOutput(outputStream, uri)
}
}
return mediaOutput
}
private fun openOutputStream(uri: Uri): OutputStream? {
return try {
contentResolver.openOutputStream(uri)
} catch (e: Exception) {
activity.showErrorToast(e)
null
}
}
private fun getFileDescriptorMediaOutput(): MediaOutput.FileDescriptorMediaOutput? {
var mediaOutput: MediaOutput.FileDescriptorMediaOutput? = null
val canWrite = canWriteToFilePath(mediaStorageDir)
if (canWrite) {
val path = activity.getOutputMediaFile(false)
val uri = getUriForFilePath(path)
if (uri != null) {
val fileDescriptor = contentResolver.openFileDescriptor(uri, MODE)
if (fileDescriptor != null) {
mediaOutput = MediaOutput.FileDescriptorMediaOutput(fileDescriptor, uri)
}
}
}
return mediaOutput
}
private fun getFileMediaOutput(): MediaOutput.FileMediaOutput? {
var mediaOutput: MediaOutput.FileMediaOutput? = null
val canWrite = canWriteToFilePath(mediaStorageDir)
if (canWrite) {
val path = activity.getOutputMediaFile(false)
val uri = getUriForFilePath(path)
if (uri != null) {
mediaOutput = MediaOutput.FileMediaOutput(File(path), uri)
}
}
return mediaOutput
}
private fun openFileDescriptor(uri: Uri): ParcelFileDescriptor? {
return try {
contentResolver.openFileDescriptor(uri, MODE)
} catch (e: Exception) {
activity.showErrorToast(e)
null
}
}
private fun canWriteToFilePath(path: String): Boolean {
return when {
activity.isRestrictedSAFOnlyRoot(path) -> activity.hasProperStoredAndroidTreeUri(path)
activity.needsStupidWritePermissions(path) -> activity.hasProperStoredTreeUri(false)
activity.isAccessibleWithSAFSdk30(path) -> activity.hasProperStoredFirstParentUri(path)
else -> File(path).canWrite()
}
}
private fun getUriForFilePath(path: String): Uri? {
val targetFile = File(path)
return when {
activity.isRestrictedSAFOnlyRoot(path) -> activity.getAndroidSAFUri(path)
activity.needsStupidWritePermissions(path) -> {
targetFile.parentFile?.let { parentFile ->
val documentFile =
if (activity.getDoesFilePathExist(parentFile.absolutePath)) {
activity.getDocumentFile(parentFile.path)
} else {
val parentDocumentFile = parentFile.parent?.let {
activity.getDocumentFile(it)
}
parentDocumentFile?.createDirectory(parentFile.name)
?: activity.getDocumentFile(parentFile.absolutePath)
}
if (documentFile == null) {
return Uri.fromFile(targetFile)
}
try {
if (activity.getDoesFilePathExist(path)) {
activity.createDocumentUriFromRootTree(path)
} else {
documentFile.createFile(path.getMimeType(), path.getFilenameFromPath())?.uri
}
} catch (e: Exception) {
null
}
}
}
activity.isAccessibleWithSAFSdk30(path) -> {
try {
activity.createDocumentUriUsingFirstParentTreeUri(path)
} catch (e: Exception) {
null
} ?: Uri.fromFile(targetFile)
}
else -> return Uri.fromFile(targetFile)
}
}
}