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

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

View File

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

View File

@ -38,4 +38,8 @@ class CameraErrorHandler(
else -> context.toast(R.string.video_recording_failed)
}
}
fun showSaveToInternalStorage() {
context.toast(R.string.save_error_internal_storage)
}
}

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ interface MyPreview {
fun onPaused() = Unit
fun setTargetUri(uri: Uri)
fun setTargetUri(uri: Uri) = Unit
fun setIsImageCaptureIntent(isImageCaptureIntent: Boolean) = Unit

View File

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

View File

@ -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" />