mirror of
https://github.com/SimpleMobileTools/Simple-Camera.git
synced 2025-02-16 11:20:55 +01:00
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:
parent
f43cd4f939
commit
889a384f21
@ -8,6 +8,7 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import android.view.KeyEvent
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.View
|
||||
@ -23,12 +24,11 @@ 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
|
||||
import com.simplemobiletools.camera.helpers.PhotoProcessor
|
||||
import com.simplemobiletools.camera.implementations.CameraXPreview
|
||||
import com.simplemobiletools.camera.implementations.CameraXInitializer
|
||||
import com.simplemobiletools.camera.implementations.CameraXPreviewListener
|
||||
import com.simplemobiletools.camera.implementations.MyCameraImpl
|
||||
import com.simplemobiletools.camera.interfaces.MyPreview
|
||||
@ -39,6 +39,7 @@ import com.simplemobiletools.commons.helpers.PERMISSION_CAMERA
|
||||
import com.simplemobiletools.commons.helpers.PERMISSION_RECORD_AUDIO
|
||||
import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE
|
||||
import com.simplemobiletools.commons.helpers.REFRESH_PATH
|
||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||
import com.simplemobiletools.commons.models.Release
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.android.synthetic.main.activity_main.btn_holder
|
||||
@ -51,7 +52,7 @@ import kotlinx.android.synthetic.main.activity_main.toggle_camera
|
||||
import kotlinx.android.synthetic.main.activity_main.toggle_flash
|
||||
import kotlinx.android.synthetic.main.activity_main.toggle_photo_video
|
||||
import kotlinx.android.synthetic.main.activity_main.video_rec_curr_timer
|
||||
import kotlinx.android.synthetic.main.activity_main.view_finder
|
||||
import kotlinx.android.synthetic.main.activity_main.preview_view
|
||||
import kotlinx.android.synthetic.main.activity_main.view_holder
|
||||
|
||||
class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener {
|
||||
@ -69,7 +70,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
private var mPreviewUri: Uri? = null
|
||||
private var mIsInPhotoMode = false
|
||||
private var mIsCameraAvailable = false
|
||||
private var mIsVideoCaptureIntent = false
|
||||
private var mIsHardwareShutterHandled = false
|
||||
private var mCurrVideoRecTimer = 0
|
||||
var mLastHandledOrientation = 0
|
||||
@ -102,7 +102,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
scheduleFadeOut()
|
||||
mFocusCircleView.setStrokeColor(getProperPrimaryColor())
|
||||
|
||||
if (mIsVideoCaptureIntent && mIsInPhotoMode) {
|
||||
if (isVideoCaptureIntent() && mIsInPhotoMode) {
|
||||
handleTogglePhotoVideo()
|
||||
checkButtons()
|
||||
}
|
||||
@ -133,9 +133,17 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
}
|
||||
|
||||
private fun initVariables() {
|
||||
mIsInPhotoMode = config.initPhotoMode
|
||||
mIsInPhotoMode = if (isVideoCaptureIntent()) {
|
||||
Log.w(TAG, "initializeCamera: video capture")
|
||||
false
|
||||
} else if (isImageCaptureIntent()) {
|
||||
Log.w(TAG, "initializeCamera: image capture mode")
|
||||
true
|
||||
} else {
|
||||
config.initPhotoMode
|
||||
}
|
||||
Log.w(TAG, "initInPhotoMode = $mIsInPhotoMode")
|
||||
mIsCameraAvailable = false
|
||||
mIsVideoCaptureIntent = false
|
||||
mIsHardwareShutterHandled = false
|
||||
mCurrVideoRecTimer = 0
|
||||
mLastHandledOrientation = 0
|
||||
@ -188,10 +196,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
}
|
||||
}
|
||||
|
||||
private fun isImageCaptureIntent() = intent?.action == MediaStore.ACTION_IMAGE_CAPTURE || intent?.action == MediaStore.ACTION_IMAGE_CAPTURE_SECURE
|
||||
private fun isImageCaptureIntent(): Boolean = intent?.action == MediaStore.ACTION_IMAGE_CAPTURE || intent?.action == MediaStore.ACTION_IMAGE_CAPTURE_SECURE
|
||||
|
||||
private fun isVideoCaptureIntent(): Boolean = intent?.action == MediaStore.ACTION_VIDEO_CAPTURE
|
||||
|
||||
private fun checkImageCaptureIntent() {
|
||||
if (isImageCaptureIntent()) {
|
||||
Log.i(TAG, "isImageCaptureIntent: ")
|
||||
hideIntentButtons()
|
||||
val output = intent.extras?.get(MediaStore.EXTRA_OUTPUT)
|
||||
if (output != null && output is Uri) {
|
||||
@ -202,7 +213,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
|
||||
private fun checkVideoCaptureIntent() {
|
||||
if (intent?.action == MediaStore.ACTION_VIDEO_CAPTURE) {
|
||||
mIsVideoCaptureIntent = true
|
||||
Log.i(TAG, "checkVideoCaptureIntent: ")
|
||||
mIsInPhotoMode = false
|
||||
hideIntentButtons()
|
||||
shutter.setImageResource(R.drawable.ic_video_rec)
|
||||
@ -221,7 +232,15 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
)
|
||||
|
||||
checkVideoCaptureIntent()
|
||||
mPreview = CameraXPreview(this, view_finder, MediaOutputHelper(this), this)
|
||||
val outputUri = intent.extras?.get(MediaStore.EXTRA_OUTPUT) as? Uri
|
||||
val is3rdPartyIntent = isVideoCaptureIntent() || isImageCaptureIntent()
|
||||
mPreview = CameraXInitializer(this).createCameraXPreview(
|
||||
preview_view,
|
||||
listener = this,
|
||||
outputUri = outputUri,
|
||||
is3rdPartyIntent = is3rdPartyIntent,
|
||||
initInPhotoMode = mIsInPhotoMode,
|
||||
)
|
||||
checkImageCaptureIntent()
|
||||
mPreview?.setIsImageCaptureIntent(isImageCaptureIntent())
|
||||
|
||||
@ -313,7 +332,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
togglePhotoVideo()
|
||||
} else {
|
||||
toast(R.string.no_audio_permissions)
|
||||
if (mIsVideoCaptureIntent) {
|
||||
if (isVideoCaptureIntent()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@ -325,7 +344,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
return
|
||||
}
|
||||
|
||||
if (mIsVideoCaptureIntent) {
|
||||
if (isVideoCaptureIntent()) {
|
||||
mPreview?.initVideoMode()
|
||||
}
|
||||
|
||||
@ -357,7 +376,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
mPreview?.initVideoMode()
|
||||
initVideoButtons()
|
||||
} catch (e: Exception) {
|
||||
if (!mIsVideoCaptureIntent) {
|
||||
if (!isVideoCaptureIntent()) {
|
||||
toast(R.string.video_mode_error)
|
||||
}
|
||||
}
|
||||
@ -545,6 +564,27 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
|
||||
override fun onMediaCaptured(uri: Uri) {
|
||||
loadLastTakenMedia(uri)
|
||||
ensureBackgroundThread {
|
||||
if (isImageCaptureIntent()) {
|
||||
val bitmap = contentResolver.loadThumbnail(uri, Size(30, 30), null)
|
||||
Intent().apply {
|
||||
data = uri
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
putExtra("data", bitmap)
|
||||
setResult(Activity.RESULT_OK, this)
|
||||
}
|
||||
Log.w(TAG, "onMediaCaptured: exiting uri=$uri")
|
||||
finish()
|
||||
} else if (isVideoCaptureIntent()) {
|
||||
Intent().apply {
|
||||
data = uri
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
setResult(Activity.RESULT_OK, this)
|
||||
}
|
||||
Log.w(TAG, "onMediaCaptured: video exiting uri=$uri")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeFlashMode(flashMode: Int) {
|
||||
@ -588,7 +628,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
||||
|
||||
fun videoSaved(uri: Uri) {
|
||||
setupPreviewImage(false)
|
||||
if (mIsVideoCaptureIntent) {
|
||||
if (isVideoCaptureIntent()) {
|
||||
Intent().apply {
|
||||
data = uri
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
@ -17,10 +17,19 @@ fun Context.getOutputMediaFile(isPhoto: Boolean): String {
|
||||
}
|
||||
}
|
||||
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
val mediaName = getRandomMediaName(isPhoto)
|
||||
return if (isPhoto) {
|
||||
"${mediaStorageDir.path}/IMG_$timestamp.jpg"
|
||||
"${mediaStorageDir.path}/$mediaName.jpg"
|
||||
} else {
|
||||
"${mediaStorageDir.path}/VID_$timestamp.mp4"
|
||||
"${mediaStorageDir.path}/$mediaName.mp4"
|
||||
}
|
||||
}
|
||||
|
||||
fun getRandomMediaName(isPhoto: Boolean): String {
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
return if (isPhoto) {
|
||||
"IMG_$timestamp"
|
||||
} else {
|
||||
"VID_$timestamp"
|
||||
}
|
||||
}
|
||||
|
@ -38,4 +38,8 @@ class CameraErrorHandler(
|
||||
else -> context.toast(R.string.video_recording_failed)
|
||||
}
|
||||
}
|
||||
|
||||
fun showSaveToInternalStorage() {
|
||||
context.toast(R.string.save_error_internal_storage)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
package com.simplemobiletools.camera.implementations
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.camera.view.PreviewView
|
||||
import com.simplemobiletools.camera.helpers.CameraErrorHandler
|
||||
import com.simplemobiletools.camera.helpers.MediaOutputHelper
|
||||
import com.simplemobiletools.commons.activities.BaseSimpleActivity
|
||||
|
||||
class CameraXInitializer(private val activity: BaseSimpleActivity) {
|
||||
|
||||
fun createCameraXPreview(
|
||||
previewView: PreviewView,
|
||||
listener: CameraXPreviewListener,
|
||||
outputUri: Uri?,
|
||||
is3rdPartyIntent: Boolean,
|
||||
initInPhotoMode: Boolean,
|
||||
): CameraXPreview {
|
||||
val cameraErrorHandler = newCameraErrorHandler()
|
||||
val mediaOutputHelper = newMediaOutputHelper(cameraErrorHandler, outputUri, is3rdPartyIntent)
|
||||
return CameraXPreview(
|
||||
activity,
|
||||
previewView,
|
||||
mediaOutputHelper,
|
||||
cameraErrorHandler,
|
||||
listener,
|
||||
initInPhotoMode,
|
||||
)
|
||||
}
|
||||
|
||||
private fun newMediaOutputHelper(
|
||||
cameraErrorHandler: CameraErrorHandler,
|
||||
outputUri: Uri?,
|
||||
is3rdPartyIntent: Boolean,
|
||||
): MediaOutputHelper {
|
||||
return MediaOutputHelper(
|
||||
activity,
|
||||
cameraErrorHandler,
|
||||
outputUri,
|
||||
is3rdPartyIntent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun newCameraErrorHandler(): CameraErrorHandler {
|
||||
return CameraErrorHandler(activity)
|
||||
}
|
||||
}
|
@ -53,6 +53,7 @@ import androidx.window.layout.WindowMetricsCalculator
|
||||
import com.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION
|
||||
import com.simplemobiletools.camera.R
|
||||
import com.simplemobiletools.camera.extensions.config
|
||||
import com.simplemobiletools.camera.extensions.getRandomMediaName
|
||||
import com.simplemobiletools.camera.extensions.toAppFlashMode
|
||||
import com.simplemobiletools.camera.extensions.toCameraSelector
|
||||
import com.simplemobiletools.camera.extensions.toLensFacing
|
||||
@ -61,7 +62,10 @@ import com.simplemobiletools.camera.helpers.MediaOutputHelper
|
||||
import com.simplemobiletools.camera.helpers.MediaSoundHelper
|
||||
import com.simplemobiletools.camera.helpers.PinchToZoomOnScaleGestureListener
|
||||
import com.simplemobiletools.camera.interfaces.MyPreview
|
||||
import com.simplemobiletools.camera.models.MediaOutput
|
||||
import com.simplemobiletools.commons.extensions.hasPermission
|
||||
import com.simplemobiletools.commons.extensions.toast
|
||||
import com.simplemobiletools.commons.helpers.PERMISSION_RECORD_AUDIO
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
@ -73,7 +77,9 @@ class CameraXPreview(
|
||||
private val activity: AppCompatActivity,
|
||||
private val previewView: PreviewView,
|
||||
private val mediaOutputHelper: MediaOutputHelper,
|
||||
private val cameraErrorHandler: CameraErrorHandler,
|
||||
private val listener: CameraXPreviewListener,
|
||||
initInPhotoMode: Boolean,
|
||||
) : MyPreview, DefaultLifecycleObserver {
|
||||
|
||||
companion object {
|
||||
@ -92,7 +98,6 @@ class CameraXPreview(
|
||||
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
private val mediaSoundHelper = MediaSoundHelper()
|
||||
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
|
||||
private val cameraErrorHandler = CameraErrorHandler(activity)
|
||||
|
||||
private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
@ -123,7 +128,9 @@ class CameraXPreview(
|
||||
private var recordingState: VideoRecordEvent? = null
|
||||
private var cameraSelector = config.lastUsedCameraLens.toCameraSelector()
|
||||
private var flashMode = FLASH_MODE_OFF
|
||||
private var isPhotoCapture = config.initPhotoMode
|
||||
private var isPhotoCapture = initInPhotoMode.also {
|
||||
Log.i(TAG, "initInPhotoMode= $it")
|
||||
}
|
||||
|
||||
init {
|
||||
bindToLifeCycle()
|
||||
@ -147,7 +154,8 @@ class CameraXPreview(
|
||||
setupCameraObservers()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "startCamera: ", e)
|
||||
activity.toast(if (switching) R.string.camera_switch_error else R.string.camera_open_error)
|
||||
val errorMessage = if (switching) R.string.camera_switch_error else R.string.camera_open_error
|
||||
activity.toast(errorMessage)
|
||||
}
|
||||
}, mainExecutor)
|
||||
}
|
||||
@ -296,10 +304,6 @@ class CameraXPreview(
|
||||
orientationEventListener.disable()
|
||||
}
|
||||
|
||||
override fun setTargetUri(uri: Uri) {
|
||||
|
||||
}
|
||||
|
||||
override fun showChangeResolutionDialog() {
|
||||
|
||||
}
|
||||
@ -347,18 +351,11 @@ class CameraXPreview(
|
||||
isReversedHorizontal = isFrontCameraInUse() && config.flipPhotos
|
||||
}
|
||||
|
||||
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 mediaOutput = mediaOutputHelper.getImageMediaOutput()
|
||||
val outputOptionsBuilder = when (mediaOutput) {
|
||||
is MediaOutput.MediaStoreOutput -> OutputFileOptions.Builder(contentResolver, mediaOutput.contentUri, mediaOutput.contentValues)
|
||||
is MediaOutput.OutputStreamMediaOutput -> OutputFileOptions.Builder(mediaOutput.outputStream)
|
||||
else -> throw IllegalArgumentException("Unexpected option for image")
|
||||
}
|
||||
|
||||
val outputOptions = outputOptionsBuilder.setMetadata(metadata).build()
|
||||
@ -366,7 +363,7 @@ class CameraXPreview(
|
||||
imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback {
|
||||
override fun onImageSaved(outputFileResults: OutputFileResults) {
|
||||
listener.toggleBottomButtons(false)
|
||||
listener.onMediaCaptured(mediaOutput?.uri ?: outputFileResults.savedUri!!)
|
||||
listener.onMediaCaptured(mediaOutput.uri ?: outputFileResults.savedUri!!)
|
||||
}
|
||||
|
||||
override fun onError(exception: ImageCaptureException) {
|
||||
@ -403,19 +400,17 @@ class CameraXPreview(
|
||||
private fun startRecording() {
|
||||
val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.")
|
||||
|
||||
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 mediaOutput = mediaOutputHelper.getVideoMediaOutput()
|
||||
val recording = when (mediaOutput) {
|
||||
is MediaOutput.FileDescriptorMediaOutput -> {
|
||||
FileDescriptorOutputOptions.Builder(mediaOutput.fileDescriptor).build()
|
||||
.let { videoCapture.output.prepareRecording(activity, it) }
|
||||
}
|
||||
val contentUri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
MediaStoreOutputOptions.Builder(contentResolver, contentUri).setContentValues(contentValues).build()
|
||||
.let { videoCapture.output.prepareRecording(activity, it) }
|
||||
is MediaOutput.MediaStoreOutput -> {
|
||||
MediaStoreOutputOptions.Builder(contentResolver, mediaOutput.contentUri).setContentValues(mediaOutput.contentValues).build()
|
||||
.let { videoCapture.output.prepareRecording(activity, it) }
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unexpected output option for video $mediaOutput")
|
||||
}
|
||||
|
||||
currentRecording = recording.withAudioEnabled()
|
||||
@ -439,7 +434,7 @@ class CameraXPreview(
|
||||
Log.e(TAG, "recording failed:", recordEvent.cause)
|
||||
cameraErrorHandler.handleVideoRecordingError(recordEvent.error)
|
||||
} else {
|
||||
listener.onMediaCaptured(mediaOutput?.uri ?: recordEvent.outputResults.outputUri)
|
||||
listener.onMediaCaptured(mediaOutput.uri ?: recordEvent.outputResults.outputUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -447,15 +442,6 @@ class CameraXPreview(
|
||||
Log.d(TAG, "Recording started")
|
||||
}
|
||||
|
||||
private fun getRandomMediaName(isPhoto: Boolean): String {
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
return if (isPhoto) {
|
||||
"IMG_$timestamp"
|
||||
} else {
|
||||
"VID_$timestamp"
|
||||
}
|
||||
}
|
||||
|
||||
private fun playShutterSoundIfEnabled() {
|
||||
if (config.isSoundEnabled) {
|
||||
mediaSoundHelper.playShutterSound()
|
||||
|
@ -8,7 +8,7 @@ interface MyPreview {
|
||||
|
||||
fun onPaused() = Unit
|
||||
|
||||
fun setTargetUri(uri: Uri)
|
||||
fun setTargetUri(uri: Uri) = Unit
|
||||
|
||||
fun setIsImageCaptureIntent(isImageCaptureIntent: Boolean) = Unit
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
package com.simplemobiletools.camera.models
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import java.io.OutputStream
|
||||
|
||||
sealed class MediaOutput(
|
||||
open val uri: Uri?,
|
||||
) {
|
||||
data class MediaStoreOutput(
|
||||
val contentValues: ContentValues,
|
||||
val contentUri: Uri,
|
||||
) : MediaOutput(null)
|
||||
|
||||
data class OutputStreamMediaOutput(
|
||||
val outputStream: OutputStream,
|
||||
override val uri: Uri,
|
||||
) : MediaOutput(uri)
|
||||
|
||||
data class FileDescriptorMediaOutput(
|
||||
val fileDescriptor: ParcelFileDescriptor,
|
||||
override val uri: Uri,
|
||||
) : MediaOutput(uri)
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
android:background="@android:color/black">
|
||||
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/view_finder"
|
||||
android:id="@+id/preview_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user