mirror of
https://github.com/SimpleMobileTools/Simple-Camera.git
synced 2025-06-27 09:02:59 +02: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:
@ -8,6 +8,7 @@ import android.os.Bundle
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.Size
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.OrientationEventListener
|
import android.view.OrientationEventListener
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -23,12 +24,11 @@ import com.simplemobiletools.camera.R
|
|||||||
import com.simplemobiletools.camera.extensions.config
|
import com.simplemobiletools.camera.extensions.config
|
||||||
import com.simplemobiletools.camera.helpers.FLASH_OFF
|
import com.simplemobiletools.camera.helpers.FLASH_OFF
|
||||||
import com.simplemobiletools.camera.helpers.FLASH_ON
|
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_LEFT
|
||||||
import com.simplemobiletools.camera.helpers.ORIENT_LANDSCAPE_RIGHT
|
import com.simplemobiletools.camera.helpers.ORIENT_LANDSCAPE_RIGHT
|
||||||
import com.simplemobiletools.camera.helpers.ORIENT_PORTRAIT
|
import com.simplemobiletools.camera.helpers.ORIENT_PORTRAIT
|
||||||
import com.simplemobiletools.camera.helpers.PhotoProcessor
|
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.CameraXPreviewListener
|
||||||
import com.simplemobiletools.camera.implementations.MyCameraImpl
|
import com.simplemobiletools.camera.implementations.MyCameraImpl
|
||||||
import com.simplemobiletools.camera.interfaces.MyPreview
|
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_RECORD_AUDIO
|
||||||
import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE
|
import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE
|
||||||
import com.simplemobiletools.commons.helpers.REFRESH_PATH
|
import com.simplemobiletools.commons.helpers.REFRESH_PATH
|
||||||
|
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||||
import com.simplemobiletools.commons.models.Release
|
import com.simplemobiletools.commons.models.Release
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlinx.android.synthetic.main.activity_main.btn_holder
|
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_flash
|
||||||
import kotlinx.android.synthetic.main.activity_main.toggle_photo_video
|
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.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
|
import kotlinx.android.synthetic.main.activity_main.view_holder
|
||||||
|
|
||||||
class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener {
|
class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener {
|
||||||
@ -69,7 +70,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
private var mPreviewUri: Uri? = null
|
private var mPreviewUri: Uri? = null
|
||||||
private var mIsInPhotoMode = false
|
private var mIsInPhotoMode = false
|
||||||
private var mIsCameraAvailable = false
|
private var mIsCameraAvailable = false
|
||||||
private var mIsVideoCaptureIntent = false
|
|
||||||
private var mIsHardwareShutterHandled = false
|
private var mIsHardwareShutterHandled = false
|
||||||
private var mCurrVideoRecTimer = 0
|
private var mCurrVideoRecTimer = 0
|
||||||
var mLastHandledOrientation = 0
|
var mLastHandledOrientation = 0
|
||||||
@ -102,7 +102,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
scheduleFadeOut()
|
scheduleFadeOut()
|
||||||
mFocusCircleView.setStrokeColor(getProperPrimaryColor())
|
mFocusCircleView.setStrokeColor(getProperPrimaryColor())
|
||||||
|
|
||||||
if (mIsVideoCaptureIntent && mIsInPhotoMode) {
|
if (isVideoCaptureIntent() && mIsInPhotoMode) {
|
||||||
handleTogglePhotoVideo()
|
handleTogglePhotoVideo()
|
||||||
checkButtons()
|
checkButtons()
|
||||||
}
|
}
|
||||||
@ -133,9 +133,17 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initVariables() {
|
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
|
mIsCameraAvailable = false
|
||||||
mIsVideoCaptureIntent = false
|
|
||||||
mIsHardwareShutterHandled = false
|
mIsHardwareShutterHandled = false
|
||||||
mCurrVideoRecTimer = 0
|
mCurrVideoRecTimer = 0
|
||||||
mLastHandledOrientation = 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() {
|
private fun checkImageCaptureIntent() {
|
||||||
if (isImageCaptureIntent()) {
|
if (isImageCaptureIntent()) {
|
||||||
|
Log.i(TAG, "isImageCaptureIntent: ")
|
||||||
hideIntentButtons()
|
hideIntentButtons()
|
||||||
val output = intent.extras?.get(MediaStore.EXTRA_OUTPUT)
|
val output = intent.extras?.get(MediaStore.EXTRA_OUTPUT)
|
||||||
if (output != null && output is Uri) {
|
if (output != null && output is Uri) {
|
||||||
@ -202,7 +213,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
|
|
||||||
private fun checkVideoCaptureIntent() {
|
private fun checkVideoCaptureIntent() {
|
||||||
if (intent?.action == MediaStore.ACTION_VIDEO_CAPTURE) {
|
if (intent?.action == MediaStore.ACTION_VIDEO_CAPTURE) {
|
||||||
mIsVideoCaptureIntent = true
|
Log.i(TAG, "checkVideoCaptureIntent: ")
|
||||||
mIsInPhotoMode = false
|
mIsInPhotoMode = false
|
||||||
hideIntentButtons()
|
hideIntentButtons()
|
||||||
shutter.setImageResource(R.drawable.ic_video_rec)
|
shutter.setImageResource(R.drawable.ic_video_rec)
|
||||||
@ -221,7 +232,15 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
)
|
)
|
||||||
|
|
||||||
checkVideoCaptureIntent()
|
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()
|
checkImageCaptureIntent()
|
||||||
mPreview?.setIsImageCaptureIntent(isImageCaptureIntent())
|
mPreview?.setIsImageCaptureIntent(isImageCaptureIntent())
|
||||||
|
|
||||||
@ -313,7 +332,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
togglePhotoVideo()
|
togglePhotoVideo()
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.no_audio_permissions)
|
toast(R.string.no_audio_permissions)
|
||||||
if (mIsVideoCaptureIntent) {
|
if (isVideoCaptureIntent()) {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,7 +344,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mIsVideoCaptureIntent) {
|
if (isVideoCaptureIntent()) {
|
||||||
mPreview?.initVideoMode()
|
mPreview?.initVideoMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,7 +376,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
mPreview?.initVideoMode()
|
mPreview?.initVideoMode()
|
||||||
initVideoButtons()
|
initVideoButtons()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (!mIsVideoCaptureIntent) {
|
if (!isVideoCaptureIntent()) {
|
||||||
toast(R.string.video_mode_error)
|
toast(R.string.video_mode_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -545,6 +564,27 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
|
|
||||||
override fun onMediaCaptured(uri: Uri) {
|
override fun onMediaCaptured(uri: Uri) {
|
||||||
loadLastTakenMedia(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) {
|
override fun onChangeFlashMode(flashMode: Int) {
|
||||||
@ -588,7 +628,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||||||
|
|
||||||
fun videoSaved(uri: Uri) {
|
fun videoSaved(uri: Uri) {
|
||||||
setupPreviewImage(false)
|
setupPreviewImage(false)
|
||||||
if (mIsVideoCaptureIntent) {
|
if (isVideoCaptureIntent()) {
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
data = uri
|
data = uri
|
||||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
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) {
|
return if (isPhoto) {
|
||||||
"${mediaStorageDir.path}/IMG_$timestamp.jpg"
|
"${mediaStorageDir.path}/$mediaName.jpg"
|
||||||
} else {
|
} 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)
|
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
|
package com.simplemobiletools.camera.helpers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.simplemobiletools.camera.extensions.config
|
import com.simplemobiletools.camera.extensions.config
|
||||||
import com.simplemobiletools.camera.extensions.getOutputMediaFile
|
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.activities.BaseSimpleActivity
|
||||||
import com.simplemobiletools.commons.extensions.createAndroidSAFFile
|
|
||||||
import com.simplemobiletools.commons.extensions.createDocumentUriFromRootTree
|
import com.simplemobiletools.commons.extensions.createDocumentUriFromRootTree
|
||||||
import com.simplemobiletools.commons.extensions.createDocumentUriUsingFirstParentTreeUri
|
import com.simplemobiletools.commons.extensions.createDocumentUriUsingFirstParentTreeUri
|
||||||
import com.simplemobiletools.commons.extensions.createSAFFileSdk30
|
|
||||||
import com.simplemobiletools.commons.extensions.getAndroidSAFUri
|
import com.simplemobiletools.commons.extensions.getAndroidSAFUri
|
||||||
import com.simplemobiletools.commons.extensions.getDocumentFile
|
import com.simplemobiletools.commons.extensions.getDocumentFile
|
||||||
import com.simplemobiletools.commons.extensions.getDoesFilePathExist
|
import com.simplemobiletools.commons.extensions.getDoesFilePathExist
|
||||||
@ -22,78 +25,159 @@ import com.simplemobiletools.commons.extensions.hasProperStoredTreeUri
|
|||||||
import com.simplemobiletools.commons.extensions.isAccessibleWithSAFSdk30
|
import com.simplemobiletools.commons.extensions.isAccessibleWithSAFSdk30
|
||||||
import com.simplemobiletools.commons.extensions.isRestrictedSAFOnlyRoot
|
import com.simplemobiletools.commons.extensions.isRestrictedSAFOnlyRoot
|
||||||
import com.simplemobiletools.commons.extensions.needsStupidWritePermissions
|
import com.simplemobiletools.commons.extensions.needsStupidWritePermissions
|
||||||
import com.simplemobiletools.commons.extensions.showFileCreateError
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
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 {
|
companion object {
|
||||||
private const val TAG = "MediaOutputHelper"
|
private const val TAG = "MediaOutputHelper"
|
||||||
private const val MODE = "rw"
|
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 mediaStorageDir = activity.config.savePhotosFolder
|
||||||
|
private val contentResolver = activity.contentResolver
|
||||||
|
|
||||||
fun getOutputStreamMediaOutput(): MediaOutput.OutputStreamMediaOutput? {
|
fun getImageMediaOutput(): MediaOutput {
|
||||||
val canWrite = activity.canWrite(mediaStorageDir)
|
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}")
|
Log.i(TAG, "getMediaOutput: canWrite=${canWrite}")
|
||||||
return if (canWrite) {
|
if (canWrite) {
|
||||||
val path = activity.getOutputMediaFile(true)
|
val path = activity.getOutputMediaFile(true)
|
||||||
val uri = activity.getUri(path)
|
val uri = getUriForFilePath(path)
|
||||||
uri?.let {
|
val outputStream = activity.getFileOutputStreamSync(path, path.getMimeType())
|
||||||
activity.getFileOutputStreamSync(path, path.getMimeType())?.let {
|
if (uri != null && outputStream != null) {
|
||||||
MediaOutput.OutputStreamMediaOutput(it, uri)
|
mediaOutput = MediaOutput.OutputStreamMediaOutput(outputStream, uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
Log.i(TAG, "OutputStreamMediaOutput: $mediaOutput")
|
||||||
|
return mediaOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openOutputStream(uri: Uri): OutputStream? {
|
||||||
|
return try {
|
||||||
|
Log.i(TAG, "uri: $uri")
|
||||||
|
contentResolver.openOutputStream(uri)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
}.also {
|
|
||||||
Log.i(TAG, "output stream: $it")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFileDescriptorMediaOutput(): MediaOutput.FileDescriptorMediaOutput? {
|
private fun getFileDescriptorMediaOutput(): MediaOutput.FileDescriptorMediaOutput? {
|
||||||
val canWrite = activity.canWrite(mediaStorageDir)
|
var mediaOutput: MediaOutput.FileDescriptorMediaOutput? = null
|
||||||
|
val canWrite = canWriteToFilePath(mediaStorageDir)
|
||||||
Log.i(TAG, "getMediaOutput: canWrite=${canWrite}")
|
Log.i(TAG, "getMediaOutput: canWrite=${canWrite}")
|
||||||
return if (canWrite) {
|
if (canWrite) {
|
||||||
val path = activity.getOutputMediaFile(false)
|
val path = activity.getOutputMediaFile(false)
|
||||||
val uri = activity.getUri(path)
|
val uri = getUriForFilePath(path)
|
||||||
uri?.let {
|
if (uri != null) {
|
||||||
activity.getFileDescriptorSync(path, path.getMimeType())?.let {
|
val fileDescriptor = contentResolver.openFileDescriptor(uri, MODE)
|
||||||
MediaOutput.FileDescriptorMediaOutput(it, uri)
|
if (fileDescriptor != null) {
|
||||||
|
mediaOutput = MediaOutput.FileDescriptorMediaOutput(fileDescriptor, uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
Log.i(TAG, "FileDescriptorMediaOutput: $mediaOutput")
|
||||||
|
return mediaOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openFileDescriptor(uri: Uri): ParcelFileDescriptor? {
|
||||||
|
return try {
|
||||||
|
Log.i(TAG, "uri: $uri")
|
||||||
|
contentResolver.openFileDescriptor(uri, MODE)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
}.also {
|
|
||||||
Log.i(TAG, "descriptor: $it")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BaseSimpleActivity.canWrite(path: String): Boolean {
|
private fun canWriteToFilePath(path: String): Boolean {
|
||||||
return when {
|
return when {
|
||||||
isRestrictedSAFOnlyRoot(path) -> hasProperStoredAndroidTreeUri(path)
|
activity.isRestrictedSAFOnlyRoot(path) -> activity.hasProperStoredAndroidTreeUri(path)
|
||||||
needsStupidWritePermissions(path) -> hasProperStoredTreeUri(false)
|
activity.needsStupidWritePermissions(path) -> activity.hasProperStoredTreeUri(false)
|
||||||
isAccessibleWithSAFSdk30(path) -> hasProperStoredFirstParentUri(path)
|
activity.isAccessibleWithSAFSdk30(path) -> activity.hasProperStoredFirstParentUri(path)
|
||||||
else -> File(path).canWrite()
|
else -> File(path).canWrite()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BaseSimpleActivity.getUri(path: String): Uri? {
|
private fun getUriForFilePath(path: String): Uri? {
|
||||||
val targetFile = File(path)
|
val targetFile = File(path)
|
||||||
return when {
|
return when {
|
||||||
isRestrictedSAFOnlyRoot(path) -> {
|
activity.isRestrictedSAFOnlyRoot(path) -> activity.getAndroidSAFUri(path)
|
||||||
getAndroidSAFUri(path)
|
activity.needsStupidWritePermissions(path) -> {
|
||||||
}
|
targetFile.parentFile?.let { parentFile ->
|
||||||
needsStupidWritePermissions(path) -> {
|
|
||||||
val parentFile = targetFile.parentFile ?: return null
|
|
||||||
val documentFile =
|
val documentFile =
|
||||||
if (getDoesFilePathExist(parentFile.absolutePath ?: return null)) {
|
if (activity.getDoesFilePathExist(parentFile.absolutePath)) {
|
||||||
getDocumentFile(parentFile.path)
|
activity.getDocumentFile(parentFile.path)
|
||||||
} else {
|
} else {
|
||||||
val parentDocumentFile = parentFile.parent?.let { getDocumentFile(it) }
|
val parentDocumentFile = parentFile.parent?.let {
|
||||||
parentDocumentFile?.createDirectory(parentFile.name) ?: getDocumentFile(parentFile.absolutePath)
|
activity.getDocumentFile(it)
|
||||||
|
}
|
||||||
|
parentDocumentFile?.createDirectory(parentFile.name)
|
||||||
|
?: activity.getDocumentFile(parentFile.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (documentFile == null) {
|
if (documentFile == null) {
|
||||||
@ -101,110 +185,26 @@ class MediaOutputHelper(private val activity: BaseSimpleActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (getDoesFilePathExist(path)) {
|
if (activity.getDoesFilePathExist(path)) {
|
||||||
createDocumentUriFromRootTree(path)
|
activity.createDocumentUriFromRootTree(path)
|
||||||
} else {
|
} else {
|
||||||
documentFile.createFile(path.getMimeType(), path.getFilenameFromPath())!!.uri
|
documentFile.createFile(path.getMimeType(), path.getFilenameFromPath())?.uri
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isAccessibleWithSAFSdk30(path) -> {
|
}
|
||||||
|
activity.isAccessibleWithSAFSdk30(path) -> {
|
||||||
try {
|
try {
|
||||||
createDocumentUriUsingFirstParentTreeUri(path)
|
activity.createDocumentUriUsingFirstParentTreeUri(path)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
} ?: Uri.fromFile(targetFile)
|
} ?: Uri.fromFile(targetFile)
|
||||||
}
|
}
|
||||||
else -> return 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION
|
||||||
import com.simplemobiletools.camera.R
|
import com.simplemobiletools.camera.R
|
||||||
import com.simplemobiletools.camera.extensions.config
|
import com.simplemobiletools.camera.extensions.config
|
||||||
|
import com.simplemobiletools.camera.extensions.getRandomMediaName
|
||||||
import com.simplemobiletools.camera.extensions.toAppFlashMode
|
import com.simplemobiletools.camera.extensions.toAppFlashMode
|
||||||
import com.simplemobiletools.camera.extensions.toCameraSelector
|
import com.simplemobiletools.camera.extensions.toCameraSelector
|
||||||
import com.simplemobiletools.camera.extensions.toLensFacing
|
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.MediaSoundHelper
|
||||||
import com.simplemobiletools.camera.helpers.PinchToZoomOnScaleGestureListener
|
import com.simplemobiletools.camera.helpers.PinchToZoomOnScaleGestureListener
|
||||||
import com.simplemobiletools.camera.interfaces.MyPreview
|
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.extensions.toast
|
||||||
|
import com.simplemobiletools.commons.helpers.PERMISSION_RECORD_AUDIO
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -73,7 +77,9 @@ class CameraXPreview(
|
|||||||
private val activity: AppCompatActivity,
|
private val activity: AppCompatActivity,
|
||||||
private val previewView: PreviewView,
|
private val previewView: PreviewView,
|
||||||
private val mediaOutputHelper: MediaOutputHelper,
|
private val mediaOutputHelper: MediaOutputHelper,
|
||||||
|
private val cameraErrorHandler: CameraErrorHandler,
|
||||||
private val listener: CameraXPreviewListener,
|
private val listener: CameraXPreviewListener,
|
||||||
|
initInPhotoMode: Boolean,
|
||||||
) : MyPreview, DefaultLifecycleObserver {
|
) : MyPreview, DefaultLifecycleObserver {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -92,7 +98,6 @@ class CameraXPreview(
|
|||||||
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||||
private val mediaSoundHelper = MediaSoundHelper()
|
private val mediaSoundHelper = MediaSoundHelper()
|
||||||
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
|
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
|
||||||
private val cameraErrorHandler = CameraErrorHandler(activity)
|
|
||||||
|
|
||||||
private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) {
|
private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) {
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@ -123,7 +128,9 @@ class CameraXPreview(
|
|||||||
private var recordingState: VideoRecordEvent? = null
|
private var recordingState: VideoRecordEvent? = null
|
||||||
private var cameraSelector = config.lastUsedCameraLens.toCameraSelector()
|
private var cameraSelector = config.lastUsedCameraLens.toCameraSelector()
|
||||||
private var flashMode = FLASH_MODE_OFF
|
private var flashMode = FLASH_MODE_OFF
|
||||||
private var isPhotoCapture = config.initPhotoMode
|
private var isPhotoCapture = initInPhotoMode.also {
|
||||||
|
Log.i(TAG, "initInPhotoMode= $it")
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
bindToLifeCycle()
|
bindToLifeCycle()
|
||||||
@ -147,7 +154,8 @@ class CameraXPreview(
|
|||||||
setupCameraObservers()
|
setupCameraObservers()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "startCamera: ", e)
|
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)
|
}, mainExecutor)
|
||||||
}
|
}
|
||||||
@ -296,10 +304,6 @@ class CameraXPreview(
|
|||||||
orientationEventListener.disable()
|
orientationEventListener.disable()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setTargetUri(uri: Uri) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showChangeResolutionDialog() {
|
override fun showChangeResolutionDialog() {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -347,18 +351,11 @@ class CameraXPreview(
|
|||||||
isReversedHorizontal = isFrontCameraInUse() && config.flipPhotos
|
isReversedHorizontal = isFrontCameraInUse() && config.flipPhotos
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaOutput = mediaOutputHelper.getOutputStreamMediaOutput()
|
val mediaOutput = mediaOutputHelper.getImageMediaOutput()
|
||||||
|
val outputOptionsBuilder = when (mediaOutput) {
|
||||||
val outputOptionsBuilder = if (mediaOutput != null) {
|
is MediaOutput.MediaStoreOutput -> OutputFileOptions.Builder(contentResolver, mediaOutput.contentUri, mediaOutput.contentValues)
|
||||||
OutputFileOptions.Builder(mediaOutput.outputStream)
|
is MediaOutput.OutputStreamMediaOutput -> OutputFileOptions.Builder(mediaOutput.outputStream)
|
||||||
} else {
|
else -> throw IllegalArgumentException("Unexpected option for image")
|
||||||
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()
|
val outputOptions = outputOptionsBuilder.setMetadata(metadata).build()
|
||||||
@ -366,7 +363,7 @@ class CameraXPreview(
|
|||||||
imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback {
|
imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback {
|
||||||
override fun onImageSaved(outputFileResults: OutputFileResults) {
|
override fun onImageSaved(outputFileResults: OutputFileResults) {
|
||||||
listener.toggleBottomButtons(false)
|
listener.toggleBottomButtons(false)
|
||||||
listener.onMediaCaptured(mediaOutput?.uri ?: outputFileResults.savedUri!!)
|
listener.onMediaCaptured(mediaOutput.uri ?: outputFileResults.savedUri!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(exception: ImageCaptureException) {
|
override fun onError(exception: ImageCaptureException) {
|
||||||
@ -403,20 +400,18 @@ class CameraXPreview(
|
|||||||
private fun startRecording() {
|
private fun startRecording() {
|
||||||
val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.")
|
val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.")
|
||||||
|
|
||||||
val mediaOutput = mediaOutputHelper.getFileDescriptorMediaOutput()
|
val mediaOutput = mediaOutputHelper.getVideoMediaOutput()
|
||||||
val recording = if (mediaOutput != null) {
|
val recording = when (mediaOutput) {
|
||||||
|
is MediaOutput.FileDescriptorMediaOutput -> {
|
||||||
FileDescriptorOutputOptions.Builder(mediaOutput.fileDescriptor).build()
|
FileDescriptorOutputOptions.Builder(mediaOutput.fileDescriptor).build()
|
||||||
.let { videoCapture.output.prepareRecording(activity, it) }
|
.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)
|
is MediaOutput.MediaStoreOutput -> {
|
||||||
MediaStoreOutputOptions.Builder(contentResolver, contentUri).setContentValues(contentValues).build()
|
MediaStoreOutputOptions.Builder(contentResolver, mediaOutput.contentUri).setContentValues(mediaOutput.contentValues).build()
|
||||||
.let { videoCapture.output.prepareRecording(activity, it) }
|
.let { videoCapture.output.prepareRecording(activity, it) }
|
||||||
}
|
}
|
||||||
|
else -> throw IllegalArgumentException("Unexpected output option for video $mediaOutput")
|
||||||
|
}
|
||||||
|
|
||||||
currentRecording = recording.withAudioEnabled()
|
currentRecording = recording.withAudioEnabled()
|
||||||
.start(mainExecutor) { recordEvent ->
|
.start(mainExecutor) { recordEvent ->
|
||||||
@ -439,7 +434,7 @@ class CameraXPreview(
|
|||||||
Log.e(TAG, "recording failed:", recordEvent.cause)
|
Log.e(TAG, "recording failed:", recordEvent.cause)
|
||||||
cameraErrorHandler.handleVideoRecordingError(recordEvent.error)
|
cameraErrorHandler.handleVideoRecordingError(recordEvent.error)
|
||||||
} else {
|
} 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")
|
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() {
|
private fun playShutterSoundIfEnabled() {
|
||||||
if (config.isSoundEnabled) {
|
if (config.isSoundEnabled) {
|
||||||
mediaSoundHelper.playShutterSound()
|
mediaSoundHelper.playShutterSound()
|
||||||
|
@ -8,7 +8,7 @@ interface MyPreview {
|
|||||||
|
|
||||||
fun onPaused() = Unit
|
fun onPaused() = Unit
|
||||||
|
|
||||||
fun setTargetUri(uri: Uri)
|
fun setTargetUri(uri: Uri) = Unit
|
||||||
|
|
||||||
fun setIsImageCaptureIntent(isImageCaptureIntent: Boolean) = 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">
|
android:background="@android:color/black">
|
||||||
|
|
||||||
<androidx.camera.view.PreviewView
|
<androidx.camera.view.PreviewView
|
||||||
android:id="@+id/view_finder"
|
android:id="@+id/preview_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user