handle 3rd party image/video capture intents

- in MediaOutputHelper,
   - add support for specifying the output URI if present in the intent
   - when the output URI is specified,
       - for Image Capture, we return a `Bitmap` as a `data` extra and also the URI as the Intent data
       - for Video Capture we return the `Uri` as the Intent data
    - if no output URI is specified in the capture intent or if there is an error while trying to access the URI, use the default location with MediaStore, so we do not return inconsistent URIs (eg, SAF tree URIs)

- add CameraXInitializer to abstract CameraXPreview initialisation logic
This commit is contained in:
darthpaul
2022-06-30 00:23:41 +01:00
parent f43cd4f939
commit 889a384f21
9 changed files with 332 additions and 222 deletions

View File

@ -1,15 +1,18 @@
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 android.util.Log
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.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
@ -22,189 +25,186 @@ 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) {
class MediaOutputHelper(
private val activity: BaseSimpleActivity,
private val errorHandler: CameraErrorHandler,
private val outputUri: Uri?,
private val is3rdPartyIntent: Boolean,
) {
companion object {
private const val TAG = "MediaOutputHelper"
private const val MODE = "rw"
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 getOutputStreamMediaOutput(): MediaOutput.OutputStreamMediaOutput? {
val canWrite = activity.canWrite(mediaStorageDir)
fun getImageMediaOutput(): MediaOutput {
return if (is3rdPartyIntent) {
if (outputUri != null) {
val outputStream = openOutputStream(outputUri)
if (outputStream != null) {
MediaOutput.OutputStreamMediaOutput(outputStream, outputUri)
} else {
errorHandler.showSaveToInternalStorage()
getMediaStoreOutput(isPhoto = true)
}
} else {
getMediaStoreOutput(isPhoto = true)
}
} else {
getOutputStreamMediaOutput() ?: getMediaStoreOutput(isPhoto = true)
}
}
fun getVideoMediaOutput(): MediaOutput {
return if (is3rdPartyIntent) {
if (outputUri != null) {
val fileDescriptor = openFileDescriptor(outputUri)
if (fileDescriptor != null) {
MediaOutput.FileDescriptorMediaOutput(fileDescriptor, outputUri)
} else {
errorHandler.showSaveToInternalStorage()
getMediaStoreOutput(isPhoto = false)
}
} else {
getMediaStoreOutput(isPhoto = false)
}
} else {
getFileDescriptorMediaOutput() ?: getMediaStoreOutput(isPhoto = false)
}
}
private fun getMediaStoreOutput(isPhoto: Boolean): MediaOutput.MediaStoreOutput {
val contentValues = getContentValues(isPhoto)
val contentUri = if (isPhoto) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
}
return MediaOutput.MediaStoreOutput(contentValues, contentUri)
}
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)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
}
}
private fun getOutputStreamMediaOutput(): MediaOutput.OutputStreamMediaOutput? {
var mediaOutput: MediaOutput.OutputStreamMediaOutput? = null
val canWrite = canWriteToFilePath(mediaStorageDir)
Log.i(TAG, "getMediaOutput: canWrite=${canWrite}")
return if (canWrite) {
if (canWrite) {
val path = activity.getOutputMediaFile(true)
val uri = activity.getUri(path)
uri?.let {
activity.getFileOutputStreamSync(path, path.getMimeType())?.let {
MediaOutput.OutputStreamMediaOutput(it, uri)
}
val uri = getUriForFilePath(path)
val outputStream = activity.getFileOutputStreamSync(path, path.getMimeType())
if (uri != null && outputStream != null) {
mediaOutput = MediaOutput.OutputStreamMediaOutput(outputStream, uri)
}
} else {
null
}.also {
Log.i(TAG, "output stream: $it")
}
Log.i(TAG, "OutputStreamMediaOutput: $mediaOutput")
return mediaOutput
}
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()
}
private fun openOutputStream(uri: Uri): OutputStream? {
return try {
contentResolver.openFileDescriptor(Uri.fromFile(targetFile), MODE)
Log.i(TAG, "uri: $uri")
contentResolver.openOutputStream(uri)
} 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)
private fun getFileDescriptorMediaOutput(): MediaOutput.FileDescriptorMediaOutput? {
var mediaOutput: MediaOutput.FileDescriptorMediaOutput? = null
val canWrite = canWriteToFilePath(mediaStorageDir)
Log.i(TAG, "getMediaOutput: canWrite=${canWrite}")
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)
}
}
}
Log.i(TAG, "FileDescriptorMediaOutput: $mediaOutput")
return mediaOutput
}
data class FileDescriptorMediaOutput(
val fileDescriptor: ParcelFileDescriptor,
override val uri: Uri,
) : MediaOutput(uri)
private fun openFileDescriptor(uri: Uri): ParcelFileDescriptor? {
return try {
Log.i(TAG, "uri: $uri")
contentResolver.openFileDescriptor(uri, MODE)
} catch (e: Exception) {
e.printStackTrace()
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) {
e.printStackTrace()
null
}
}
}
activity.isAccessibleWithSAFSdk30(path) -> {
try {
activity.createDocumentUriUsingFirstParentTreeUri(path)
} catch (e: Exception) {
e.printStackTrace()
null
} ?: Uri.fromFile(targetFile)
}
else -> return Uri.fromFile(targetFile)
}
}
}