handle storage location

- add MediaOutputHelper
   - to create OutputStream for photos
   - to create FileDescriptor for videos (currently requires API 26+)
This commit is contained in:
darthpaul 2022-06-26 10:58:17 +01:00
parent e691fc1e8c
commit 79f9267383
3 changed files with 260 additions and 27 deletions

View File

@ -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())

View File

@ -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)
}
}

View File

@ -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)
}
}
}