handle storage location
- add MediaOutputHelper - to create OutputStream for photos - to create FileDescriptor for videos (currently requires API 26+)
This commit is contained in:
parent
e691fc1e8c
commit
79f9267383
|
@ -23,6 +23,7 @@ import com.simplemobiletools.camera.R
|
|||
import com.simplemobiletools.camera.extensions.config
|
||||
import com.simplemobiletools.camera.helpers.FLASH_OFF
|
||||
import com.simplemobiletools.camera.helpers.FLASH_ON
|
||||
import com.simplemobiletools.camera.helpers.MediaOutputHelper
|
||||
import com.simplemobiletools.camera.helpers.ORIENT_LANDSCAPE_LEFT
|
||||
import com.simplemobiletools.camera.helpers.ORIENT_LANDSCAPE_RIGHT
|
||||
import com.simplemobiletools.camera.helpers.ORIENT_PORTRAIT
|
||||
|
@ -220,7 +221,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||
)
|
||||
|
||||
checkVideoCaptureIntent()
|
||||
mPreview = CameraXPreview(this, view_finder, this)
|
||||
mPreview = CameraXPreview(this, view_finder, MediaOutputHelper(this), this)
|
||||
checkImageCaptureIntent()
|
||||
mPreview?.setIsImageCaptureIntent(isImageCaptureIntent())
|
||||
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
package com.simplemobiletools.camera.helpers
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.simplemobiletools.camera.extensions.config
|
||||
import com.simplemobiletools.camera.extensions.getOutputMediaFile
|
||||
import com.simplemobiletools.commons.activities.BaseSimpleActivity
|
||||
import com.simplemobiletools.commons.extensions.createAndroidSAFFile
|
||||
import com.simplemobiletools.commons.extensions.createDocumentUriFromRootTree
|
||||
import com.simplemobiletools.commons.extensions.createDocumentUriUsingFirstParentTreeUri
|
||||
import com.simplemobiletools.commons.extensions.createSAFFileSdk30
|
||||
import com.simplemobiletools.commons.extensions.getAndroidSAFUri
|
||||
import com.simplemobiletools.commons.extensions.getDocumentFile
|
||||
import com.simplemobiletools.commons.extensions.getDoesFilePathExist
|
||||
import com.simplemobiletools.commons.extensions.getFileOutputStreamSync
|
||||
import com.simplemobiletools.commons.extensions.getFilenameFromPath
|
||||
import com.simplemobiletools.commons.extensions.getMimeType
|
||||
import com.simplemobiletools.commons.extensions.hasProperStoredAndroidTreeUri
|
||||
import com.simplemobiletools.commons.extensions.hasProperStoredFirstParentUri
|
||||
import com.simplemobiletools.commons.extensions.hasProperStoredTreeUri
|
||||
import com.simplemobiletools.commons.extensions.isAccessibleWithSAFSdk30
|
||||
import com.simplemobiletools.commons.extensions.isRestrictedSAFOnlyRoot
|
||||
import com.simplemobiletools.commons.extensions.needsStupidWritePermissions
|
||||
import com.simplemobiletools.commons.extensions.showFileCreateError
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
|
||||
class MediaOutputHelper(private val activity: BaseSimpleActivity) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MediaOutputHelper"
|
||||
private const val MODE = "rw"
|
||||
}
|
||||
|
||||
private val mediaStorageDir = activity.config.savePhotosFolder
|
||||
|
||||
fun getOutputStreamMediaOutput(): MediaOutput.OutputStreamMediaOutput? {
|
||||
val canWrite = activity.canWrite(mediaStorageDir)
|
||||
Log.i(TAG, "getMediaOutput: canWrite=${canWrite}")
|
||||
return if (canWrite) {
|
||||
val path = activity.getOutputMediaFile(true)
|
||||
val uri = activity.getUri(path)
|
||||
uri?.let {
|
||||
activity.getFileOutputStreamSync(path, path.getMimeType())?.let {
|
||||
MediaOutput.OutputStreamMediaOutput(it, uri)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}.also {
|
||||
Log.i(TAG, "output stream: $it")
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileDescriptorMediaOutput(): MediaOutput.FileDescriptorMediaOutput? {
|
||||
val canWrite = activity.canWrite(mediaStorageDir)
|
||||
Log.i(TAG, "getMediaOutput: canWrite=${canWrite}")
|
||||
return if (canWrite) {
|
||||
val path = activity.getOutputMediaFile(false)
|
||||
val uri = activity.getUri(path)
|
||||
uri?.let {
|
||||
activity.getFileDescriptorSync(path, path.getMimeType())?.let {
|
||||
MediaOutput.FileDescriptorMediaOutput(it, uri)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}.also {
|
||||
Log.i(TAG, "descriptor: $it")
|
||||
}
|
||||
}
|
||||
|
||||
private fun BaseSimpleActivity.canWrite(path: String): Boolean {
|
||||
return when {
|
||||
isRestrictedSAFOnlyRoot(path) -> hasProperStoredAndroidTreeUri(path)
|
||||
needsStupidWritePermissions(path) -> hasProperStoredTreeUri(false)
|
||||
isAccessibleWithSAFSdk30(path) -> hasProperStoredFirstParentUri(path)
|
||||
else -> File(path).canWrite()
|
||||
}
|
||||
}
|
||||
|
||||
private fun BaseSimpleActivity.getUri(path: String): Uri? {
|
||||
val targetFile = File(path)
|
||||
return when {
|
||||
isRestrictedSAFOnlyRoot(path) -> {
|
||||
getAndroidSAFUri(path)
|
||||
}
|
||||
needsStupidWritePermissions(path) -> {
|
||||
val parentFile = targetFile.parentFile ?: return null
|
||||
val documentFile =
|
||||
if (getDoesFilePathExist(parentFile.absolutePath ?: return null)) {
|
||||
getDocumentFile(parentFile.path)
|
||||
} else {
|
||||
val parentDocumentFile = parentFile.parent?.let { getDocumentFile(it) }
|
||||
parentDocumentFile?.createDirectory(parentFile.name) ?: getDocumentFile(parentFile.absolutePath)
|
||||
}
|
||||
|
||||
if (documentFile == null) {
|
||||
return Uri.fromFile(targetFile)
|
||||
}
|
||||
|
||||
try {
|
||||
if (getDoesFilePathExist(path)) {
|
||||
createDocumentUriFromRootTree(path)
|
||||
} else {
|
||||
documentFile.createFile(path.getMimeType(), path.getFilenameFromPath())!!.uri
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
isAccessibleWithSAFSdk30(path) -> {
|
||||
try {
|
||||
createDocumentUriUsingFirstParentTreeUri(path)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: Uri.fromFile(targetFile)
|
||||
}
|
||||
else -> return Uri.fromFile(targetFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BaseSimpleActivity.getFileDescriptorSync(path: String, mimeType: String): ParcelFileDescriptor? {
|
||||
val targetFile = File(path)
|
||||
|
||||
return when {
|
||||
isRestrictedSAFOnlyRoot(path) -> {
|
||||
val uri = getAndroidSAFUri(path)
|
||||
if (!getDoesFilePathExist(path)) {
|
||||
createAndroidSAFFile(path)
|
||||
}
|
||||
applicationContext.contentResolver.openFileDescriptor(uri, MODE)
|
||||
}
|
||||
needsStupidWritePermissions(path) -> {
|
||||
val parentFile = targetFile.parentFile ?: return null
|
||||
val documentFile =
|
||||
if (getDoesFilePathExist(parentFile.absolutePath ?: return null)) {
|
||||
getDocumentFile(parentFile.path)
|
||||
} else {
|
||||
val parentDocumentFile = parentFile.parent?.let { getDocumentFile(it) }
|
||||
parentDocumentFile?.createDirectory(parentFile.name) ?: getDocumentFile(parentFile.absolutePath)
|
||||
}
|
||||
|
||||
|
||||
if (documentFile == null) {
|
||||
val casualOutputStream = createCasualFileDescriptor(targetFile)
|
||||
return if (casualOutputStream == null) {
|
||||
showFileCreateError(parentFile.path)
|
||||
null
|
||||
} else {
|
||||
casualOutputStream
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val uri = if (getDoesFilePathExist(path)) {
|
||||
createDocumentUriFromRootTree(path)
|
||||
} else {
|
||||
documentFile.createFile(mimeType, path.getFilenameFromPath())!!.uri
|
||||
}
|
||||
applicationContext.contentResolver.openFileDescriptor(uri, MODE)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
isAccessibleWithSAFSdk30(path) -> {
|
||||
try {
|
||||
val uri = createDocumentUriUsingFirstParentTreeUri(path)
|
||||
if (!getDoesFilePathExist(path)) {
|
||||
createSAFFileSdk30(path)
|
||||
}
|
||||
applicationContext.contentResolver.openFileDescriptor(uri, MODE)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
} ?: createCasualFileDescriptor(targetFile)
|
||||
}
|
||||
else -> return createCasualFileDescriptor(targetFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BaseSimpleActivity.createCasualFileDescriptor(targetFile: File): ParcelFileDescriptor? {
|
||||
if (targetFile.parentFile?.exists() == false) {
|
||||
targetFile.parentFile?.mkdirs()
|
||||
}
|
||||
|
||||
return try {
|
||||
contentResolver.openFileDescriptor(Uri.fromFile(targetFile), MODE)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MediaOutput(
|
||||
open val uri: Uri,
|
||||
) {
|
||||
data class OutputStreamMediaOutput(
|
||||
val outputStream: OutputStream,
|
||||
override val uri: Uri,
|
||||
) : MediaOutput(uri)
|
||||
|
||||
data class FileDescriptorMediaOutput(
|
||||
val fileDescriptor: ParcelFileDescriptor,
|
||||
override val uri: Uri,
|
||||
) : MediaOutput(uri)
|
||||
}
|
||||
}
|
|
@ -17,7 +17,13 @@ import android.view.OrientationEventListener
|
|||
import android.view.ScaleGestureDetector
|
||||
import android.view.Surface
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.core.AspectRatio
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.CameraState
|
||||
import androidx.camera.core.DisplayOrientedMeteringPointFactory
|
||||
import androidx.camera.core.FocusMeteringAction
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
|
||||
import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
|
||||
import androidx.camera.core.ImageCapture.FLASH_MODE_OFF
|
||||
|
@ -26,7 +32,11 @@ import androidx.camera.core.ImageCapture.Metadata
|
|||
import androidx.camera.core.ImageCapture.OnImageSavedCallback
|
||||
import androidx.camera.core.ImageCapture.OutputFileOptions
|
||||
import androidx.camera.core.ImageCapture.OutputFileResults
|
||||
import androidx.camera.core.ImageCaptureException
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.core.UseCase
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.video.FileDescriptorOutputOptions
|
||||
import androidx.camera.video.MediaStoreOutputOptions
|
||||
import androidx.camera.video.Quality
|
||||
import androidx.camera.video.QualitySelector
|
||||
|
@ -35,6 +45,7 @@ import androidx.camera.video.Recording
|
|||
import androidx.camera.video.VideoCapture
|
||||
import androidx.camera.video.VideoRecordEvent
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
@ -46,6 +57,7 @@ import com.simplemobiletools.camera.extensions.toAppFlashMode
|
|||
import com.simplemobiletools.camera.extensions.toCameraSelector
|
||||
import com.simplemobiletools.camera.extensions.toCameraXFlashMode
|
||||
import com.simplemobiletools.camera.extensions.toLensFacing
|
||||
import com.simplemobiletools.camera.helpers.MediaOutputHelper
|
||||
import com.simplemobiletools.camera.helpers.MediaSoundHelper
|
||||
import com.simplemobiletools.camera.helpers.PinchToZoomOnScaleGestureListener
|
||||
import com.simplemobiletools.camera.interfaces.MyPreview
|
||||
|
@ -61,6 +73,7 @@ import kotlin.math.min
|
|||
class CameraXPreview(
|
||||
private val activity: AppCompatActivity,
|
||||
private val previewView: PreviewView,
|
||||
private val mediaOutputHelper: MediaOutputHelper,
|
||||
private val listener: CameraXPreviewListener,
|
||||
) : MyPreview, DefaultLifecycleObserver {
|
||||
|
||||
|
@ -76,7 +89,7 @@ class CameraXPreview(
|
|||
|
||||
private val config = activity.config
|
||||
private val contentResolver = activity.contentResolver
|
||||
private val mainExecutor = activity.mainExecutor
|
||||
private val mainExecutor = ContextCompat.getMainExecutor(activity)
|
||||
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
private val mediaSoundHelper = MediaSoundHelper()
|
||||
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
|
||||
|
@ -363,24 +376,29 @@ class CameraXPreview(
|
|||
val imageCapture = imageCapture ?: throw IllegalStateException("Camera initialization failed.")
|
||||
|
||||
val metadata = Metadata().apply {
|
||||
isReversedHorizontal = config.flipPhotos
|
||||
isReversedHorizontal = isFrontCameraInUse() && config.flipPhotos
|
||||
}
|
||||
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(true))
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
|
||||
}
|
||||
val contentUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
val outputOptions = OutputFileOptions.Builder(contentResolver, contentUri, contentValues)
|
||||
.setMetadata(metadata)
|
||||
.build()
|
||||
val mediaOutput = mediaOutputHelper.getOutputStreamMediaOutput()
|
||||
|
||||
val outputOptionsBuilder = if (mediaOutput != null) {
|
||||
OutputFileOptions.Builder(mediaOutput.outputStream)
|
||||
} else {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(true))
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
|
||||
}
|
||||
val contentUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
OutputFileOptions.Builder(contentResolver, contentUri, contentValues)
|
||||
}
|
||||
|
||||
val outputOptions = outputOptionsBuilder.setMetadata(metadata).build()
|
||||
|
||||
imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback {
|
||||
override fun onImageSaved(outputFileResults: OutputFileResults) {
|
||||
listener.toggleBottomButtons(false)
|
||||
listener.onMediaCaptured(outputFileResults.savedUri!!)
|
||||
listener.onMediaCaptured(mediaOutput?.uri ?: outputFileResults.savedUri!!)
|
||||
}
|
||||
|
||||
override fun onError(exception: ImageCaptureException) {
|
||||
|
@ -416,19 +434,23 @@ class CameraXPreview(
|
|||
@SuppressLint("MissingPermission")
|
||||
private fun startRecording() {
|
||||
val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.")
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(false))
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
|
||||
}
|
||||
val contentUri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
val outputOptions = MediaStoreOutputOptions.Builder(contentResolver, contentUri)
|
||||
.setContentValues(contentValues)
|
||||
.build()
|
||||
|
||||
currentRecording = videoCapture.output
|
||||
.prepareRecording(activity, outputOptions)
|
||||
.withAudioEnabled()
|
||||
val mediaOutput = mediaOutputHelper.getFileDescriptorMediaOutput()
|
||||
val recording = if (mediaOutput != null) {
|
||||
FileDescriptorOutputOptions.Builder(mediaOutput.fileDescriptor).build()
|
||||
.let { videoCapture.output.prepareRecording(activity, it) }
|
||||
} else {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(false))
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
|
||||
}
|
||||
val contentUri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
MediaStoreOutputOptions.Builder(contentResolver, contentUri).setContentValues(contentValues).build()
|
||||
.let { videoCapture.output.prepareRecording(activity, it) }
|
||||
}
|
||||
|
||||
currentRecording = recording.withAudioEnabled()
|
||||
.start(mainExecutor) { recordEvent ->
|
||||
Log.d(TAG, "recordEvent=$recordEvent ")
|
||||
recordingState = recordEvent
|
||||
|
@ -448,7 +470,7 @@ class CameraXPreview(
|
|||
if (recordEvent.hasError()) {
|
||||
// TODO: Handle errors
|
||||
} else {
|
||||
listener.onMediaCaptured(recordEvent.outputResults.outputUri)
|
||||
listener.onMediaCaptured(mediaOutput?.uri ?: recordEvent.outputResults.outputUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue