commit
198faec306
|
@ -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
|
||||
|
@ -27,7 +28,7 @@ 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
|
||||
|
@ -38,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
|
||||
|
@ -50,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 {
|
||||
|
@ -68,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
|
||||
|
@ -101,7 +102,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||
scheduleFadeOut()
|
||||
mFocusCircleView.setStrokeColor(getProperPrimaryColor())
|
||||
|
||||
if (mIsVideoCaptureIntent && mIsInPhotoMode) {
|
||||
if (isVideoCaptureIntent() && mIsInPhotoMode) {
|
||||
handleTogglePhotoVideo()
|
||||
checkButtons()
|
||||
}
|
||||
|
@ -132,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
|
||||
|
@ -187,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) {
|
||||
|
@ -201,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)
|
||||
|
@ -220,7 +232,15 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||
)
|
||||
|
||||
checkVideoCaptureIntent()
|
||||
mPreview = CameraXPreview(this, view_finder, 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())
|
||||
|
||||
|
@ -235,7 +255,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||
mFadeHandler = Handler()
|
||||
setupPreviewImage(true)
|
||||
|
||||
val initialFlashlightState = config.flashlightState
|
||||
val initialFlashlightState = FLASH_OFF
|
||||
mPreview!!.setFlashlightState(initialFlashlightState)
|
||||
updateFlashlightState(initialFlashlightState)
|
||||
}
|
||||
|
@ -312,7 +332,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||
togglePhotoVideo()
|
||||
} else {
|
||||
toast(R.string.no_audio_permissions)
|
||||
if (mIsVideoCaptureIntent) {
|
||||
if (isVideoCaptureIntent()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
@ -324,7 +344,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
|
|||
return
|
||||
}
|
||||
|
||||
if (mIsVideoCaptureIntent) {
|
||||
if (isVideoCaptureIntent()) {
|
||||
mPreview?.initVideoMode()
|
||||
}
|
||||
|
||||
|
@ -356,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)
|
||||
}
|
||||
}
|
||||
|
@ -544,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) {
|
||||
|
@ -587,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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package com.simplemobiletools.camera.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.camera.core.CameraState
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.video.VideoRecordEvent
|
||||
import com.simplemobiletools.camera.R
|
||||
import com.simplemobiletools.commons.extensions.toast
|
||||
|
||||
class CameraErrorHandler(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
fun handleCameraError(error: CameraState.StateError?) {
|
||||
when (error?.code) {
|
||||
CameraState.ERROR_MAX_CAMERAS_IN_USE,
|
||||
CameraState.ERROR_CAMERA_IN_USE -> context.toast(R.string.camera_in_use_error, Toast.LENGTH_LONG)
|
||||
CameraState.ERROR_CAMERA_FATAL_ERROR -> context.toast(R.string.camera_unavailable)
|
||||
CameraState.ERROR_STREAM_CONFIG -> context.toast(R.string.camera_configure_error)
|
||||
CameraState.ERROR_CAMERA_DISABLED -> context.toast(R.string.camera_disabled_by_admin_error)
|
||||
CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> context.toast(R.string.camera_dnd_error, Toast.LENGTH_LONG)
|
||||
CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleImageCaptureError(imageCaptureError: Int) {
|
||||
when (imageCaptureError) {
|
||||
ImageCapture.ERROR_FILE_IO -> context.toast(R.string.photo_not_saved)
|
||||
else -> context.toast(R.string.photo_capture_failed)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleVideoRecordingError(error: Int) {
|
||||
when (error) {
|
||||
VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE -> context.toast(R.string.video_capture_insufficient_storage_error)
|
||||
VideoRecordEvent.Finalize.ERROR_NONE -> {}
|
||||
else -> context.toast(R.string.video_recording_failed)
|
||||
}
|
||||
}
|
||||
|
||||
fun showSaveToInternalStorage() {
|
||||
context.toast(R.string.save_error_internal_storage)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
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.createDocumentUriFromRootTree
|
||||
import com.simplemobiletools.commons.extensions.createDocumentUriUsingFirstParentTreeUri
|
||||
import com.simplemobiletools.commons.extensions.getAndroidSAFUri
|
||||
import com.simplemobiletools.commons.extensions.getDocumentFile
|
||||
import com.simplemobiletools.commons.extensions.getDoesFilePathExist
|
||||
import com.simplemobiletools.commons.extensions.getFileOutputStreamSync
|
||||
import com.simplemobiletools.commons.extensions.getFilenameFromPath
|
||||
import com.simplemobiletools.commons.extensions.getMimeType
|
||||
import com.simplemobiletools.commons.extensions.hasProperStoredAndroidTreeUri
|
||||
import com.simplemobiletools.commons.extensions.hasProperStoredFirstParentUri
|
||||
import com.simplemobiletools.commons.extensions.hasProperStoredTreeUri
|
||||
import com.simplemobiletools.commons.extensions.isAccessibleWithSAFSdk30
|
||||
import com.simplemobiletools.commons.extensions.isRestrictedSAFOnlyRoot
|
||||
import com.simplemobiletools.commons.extensions.needsStupidWritePermissions
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
|
||||
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 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}")
|
||||
if (canWrite) {
|
||||
val path = activity.getOutputMediaFile(true)
|
||||
val uri = getUriForFilePath(path)
|
||||
val outputStream = activity.getFileOutputStreamSync(path, path.getMimeType())
|
||||
if (uri != null && outputStream != null) {
|
||||
mediaOutput = MediaOutput.OutputStreamMediaOutput(outputStream, uri)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -17,7 +17,13 @@ import android.view.OrientationEventListener
|
|||
import android.view.ScaleGestureDetector
|
||||
import android.view.Surface
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.core.AspectRatio
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.CameraState
|
||||
import androidx.camera.core.DisplayOrientedMeteringPointFactory
|
||||
import androidx.camera.core.FocusMeteringAction
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
|
||||
import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
|
||||
import androidx.camera.core.ImageCapture.FLASH_MODE_OFF
|
||||
|
@ -26,7 +32,11 @@ import androidx.camera.core.ImageCapture.Metadata
|
|||
import androidx.camera.core.ImageCapture.OnImageSavedCallback
|
||||
import androidx.camera.core.ImageCapture.OutputFileOptions
|
||||
import androidx.camera.core.ImageCapture.OutputFileResults
|
||||
import androidx.camera.core.ImageCaptureException
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.core.UseCase
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.video.FileDescriptorOutputOptions
|
||||
import androidx.camera.video.MediaStoreOutputOptions
|
||||
import androidx.camera.video.Quality
|
||||
import androidx.camera.video.QualitySelector
|
||||
|
@ -35,6 +45,7 @@ import androidx.camera.video.Recording
|
|||
import androidx.camera.video.VideoCapture
|
||||
import androidx.camera.video.VideoRecordEvent
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
@ -42,15 +53,19 @@ 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.toCameraXFlashMode
|
||||
import com.simplemobiletools.camera.extensions.toLensFacing
|
||||
import com.simplemobiletools.camera.helpers.CameraErrorHandler
|
||||
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.commons.extensions.showErrorToast
|
||||
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
|
||||
|
@ -61,7 +76,10 @@ import kotlin.math.min
|
|||
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 {
|
||||
|
@ -76,7 +94,7 @@ class CameraXPreview(
|
|||
|
||||
private val config = activity.config
|
||||
private val contentResolver = activity.contentResolver
|
||||
private val mainExecutor = activity.mainExecutor
|
||||
private val mainExecutor = ContextCompat.getMainExecutor(activity)
|
||||
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
private val mediaSoundHelper = MediaSoundHelper()
|
||||
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
|
||||
|
@ -109,8 +127,10 @@ class CameraXPreview(
|
|||
private var currentRecording: Recording? = null
|
||||
private var recordingState: VideoRecordEvent? = null
|
||||
private var cameraSelector = config.lastUsedCameraLens.toCameraSelector()
|
||||
private var flashMode = config.flashlightState.toCameraXFlashMode()
|
||||
private var isPhotoCapture = config.initPhotoMode
|
||||
private var flashMode = FLASH_MODE_OFF
|
||||
private var isPhotoCapture = initInPhotoMode.also {
|
||||
Log.i(TAG, "initInPhotoMode= $it")
|
||||
}
|
||||
|
||||
init {
|
||||
bindToLifeCycle()
|
||||
|
@ -124,7 +144,7 @@ class CameraXPreview(
|
|||
activity.lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
private fun startCamera() {
|
||||
private fun startCamera(switching: Boolean = false) {
|
||||
Log.i(TAG, "startCamera: ")
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
|
||||
cameraProviderFuture.addListener({
|
||||
|
@ -134,7 +154,8 @@ class CameraXPreview(
|
|||
setupCameraObservers()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "startCamera: ", e)
|
||||
activity.showErrorToast(activity.getString(R.string.camera_open_error))
|
||||
val errorMessage = if (switching) R.string.camera_switch_error else R.string.camera_open_error
|
||||
activity.toast(errorMessage)
|
||||
}
|
||||
}, mainExecutor)
|
||||
}
|
||||
|
@ -176,48 +197,7 @@ class CameraXPreview(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Handle errors
|
||||
cameraState.error?.let { error ->
|
||||
listener.setCameraAvailable(false)
|
||||
when (error.code) {
|
||||
CameraState.ERROR_STREAM_CONFIG -> {
|
||||
Log.e(TAG, "ERROR_STREAM_CONFIG")
|
||||
// Make sure to setup the use cases properly
|
||||
activity.toast(R.string.camera_unavailable)
|
||||
}
|
||||
CameraState.ERROR_CAMERA_IN_USE -> {
|
||||
Log.e(TAG, "ERROR_CAMERA_IN_USE")
|
||||
// Close the camera or ask user to close another camera app that's using the
|
||||
// camera
|
||||
activity.showErrorToast("Camera is in use by another app, please close")
|
||||
}
|
||||
CameraState.ERROR_MAX_CAMERAS_IN_USE -> {
|
||||
Log.e(TAG, "ERROR_MAX_CAMERAS_IN_USE")
|
||||
// Close another open camera in the app, or ask the user to close another
|
||||
// camera app that's using the camera
|
||||
activity.showErrorToast("Camera is in use by another app, please close")
|
||||
}
|
||||
CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> {
|
||||
Log.e(TAG, "ERROR_OTHER_RECOVERABLE_ERROR")
|
||||
activity.toast(R.string.camera_open_error)
|
||||
}
|
||||
CameraState.ERROR_CAMERA_DISABLED -> {
|
||||
Log.e(TAG, "ERROR_CAMERA_DISABLED")
|
||||
// Ask the user to enable the device's cameras
|
||||
activity.toast(R.string.camera_open_error)
|
||||
}
|
||||
CameraState.ERROR_CAMERA_FATAL_ERROR -> {
|
||||
Log.e(TAG, "ERROR_CAMERA_FATAL_ERROR")
|
||||
// Ask the user to reboot the device to restore camera function
|
||||
activity.toast(R.string.camera_open_error)
|
||||
}
|
||||
CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> {
|
||||
// Ask the user to disable the "Do Not Disturb" mode, then reopen the camera
|
||||
Log.e(TAG, "ERROR_DO_NOT_DISTURB_MODE_ENABLED")
|
||||
activity.toast(R.string.camera_open_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
cameraErrorHandler.handleCameraError(cameraState?.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,10 +304,6 @@ class CameraXPreview(
|
|||
orientationEventListener.disable()
|
||||
}
|
||||
|
||||
override fun setTargetUri(uri: Uri) {
|
||||
|
||||
}
|
||||
|
||||
override fun showChangeResolutionDialog() {
|
||||
|
||||
}
|
||||
|
@ -340,17 +316,26 @@ class CameraXPreview(
|
|||
}
|
||||
cameraSelector = newCameraSelector
|
||||
config.lastUsedCameraLens = newCameraSelector.toLensFacing()
|
||||
startCamera()
|
||||
startCamera(switching = true)
|
||||
}
|
||||
|
||||
override fun toggleFlashlight() {
|
||||
val newFlashMode = when (flashMode) {
|
||||
FLASH_MODE_OFF -> FLASH_MODE_ON
|
||||
FLASH_MODE_ON -> FLASH_MODE_AUTO
|
||||
FLASH_MODE_AUTO -> FLASH_MODE_OFF
|
||||
else -> throw IllegalArgumentException("Unknown mode: $flashMode")
|
||||
val newFlashMode = if (isPhotoCapture) {
|
||||
when (flashMode) {
|
||||
FLASH_MODE_OFF -> FLASH_MODE_ON
|
||||
FLASH_MODE_ON -> FLASH_MODE_AUTO
|
||||
FLASH_MODE_AUTO -> FLASH_MODE_OFF
|
||||
else -> throw IllegalArgumentException("Unknown mode: $flashMode")
|
||||
}
|
||||
} else {
|
||||
when (flashMode) {
|
||||
FLASH_MODE_OFF -> FLASH_MODE_ON
|
||||
FLASH_MODE_ON -> FLASH_MODE_OFF
|
||||
else -> throw IllegalArgumentException("Unknown mode: $flashMode")
|
||||
}.also {
|
||||
camera?.cameraControl?.enableTorch(it == FLASH_MODE_ON)
|
||||
}
|
||||
}
|
||||
|
||||
flashMode = newFlashMode
|
||||
imageCapture?.flashMode = newFlashMode
|
||||
val appFlashMode = flashMode.toAppFlashMode()
|
||||
|
@ -363,30 +348,28 @@ class CameraXPreview(
|
|||
val imageCapture = imageCapture ?: throw IllegalStateException("Camera initialization failed.")
|
||||
|
||||
val metadata = Metadata().apply {
|
||||
isReversedHorizontal = config.flipPhotos
|
||||
isReversedHorizontal = isFrontCameraInUse() && config.flipPhotos
|
||||
}
|
||||
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(true))
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
|
||||
val 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 contentUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
val outputOptions = OutputFileOptions.Builder(contentResolver, contentUri, contentValues)
|
||||
.setMetadata(metadata)
|
||||
.build()
|
||||
|
||||
val outputOptions = outputOptionsBuilder.setMetadata(metadata).build()
|
||||
|
||||
imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback {
|
||||
override fun onImageSaved(outputFileResults: OutputFileResults) {
|
||||
listener.toggleBottomButtons(false)
|
||||
listener.onMediaCaptured(outputFileResults.savedUri!!)
|
||||
listener.onMediaCaptured(mediaOutput.uri ?: outputFileResults.savedUri!!)
|
||||
}
|
||||
|
||||
override fun onError(exception: ImageCaptureException) {
|
||||
listener.toggleBottomButtons(false)
|
||||
activity.showErrorToast("Capture picture $exception")
|
||||
Log.e(TAG, "Error", exception)
|
||||
listener.toggleBottomButtons(false)
|
||||
cameraErrorHandler.handleImageCaptureError(exception.imageCaptureError)
|
||||
}
|
||||
})
|
||||
playShutterSoundIfEnabled()
|
||||
|
@ -416,19 +399,21 @@ class CameraXPreview(
|
|||
@SuppressLint("MissingPermission")
|
||||
private fun startRecording() {
|
||||
val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.")
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(false))
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
|
||||
}
|
||||
val contentUri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
val outputOptions = MediaStoreOutputOptions.Builder(contentResolver, contentUri)
|
||||
.setContentValues(contentValues)
|
||||
.build()
|
||||
|
||||
currentRecording = videoCapture.output
|
||||
.prepareRecording(activity, outputOptions)
|
||||
.withAudioEnabled()
|
||||
val mediaOutput = mediaOutputHelper.getVideoMediaOutput()
|
||||
val recording = when (mediaOutput) {
|
||||
is MediaOutput.FileDescriptorMediaOutput -> {
|
||||
FileDescriptorOutputOptions.Builder(mediaOutput.fileDescriptor).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()
|
||||
.start(mainExecutor) { recordEvent ->
|
||||
Log.d(TAG, "recordEvent=$recordEvent ")
|
||||
recordingState = recordEvent
|
||||
|
@ -446,9 +431,10 @@ class CameraXPreview(
|
|||
playStopVideoRecordingSoundIfEnabled()
|
||||
listener.onVideoRecordingStopped()
|
||||
if (recordEvent.hasError()) {
|
||||
// TODO: Handle errors
|
||||
Log.e(TAG, "recording failed:", recordEvent.cause)
|
||||
cameraErrorHandler.handleVideoRecordingError(recordEvent.error)
|
||||
} else {
|
||||
listener.onMediaCaptured(recordEvent.outputResults.outputUri)
|
||||
listener.onMediaCaptured(mediaOutput.uri ?: recordEvent.outputResults.outputUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -456,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" />
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
<resources>
|
||||
<string name="app_name">Simple Camera</string>
|
||||
<string name="app_launcher_name">Camera</string>
|
||||
|
||||
<!--Errors-->
|
||||
<string name="camera_unavailable">Camera unavailable</string>
|
||||
<string name="camera_open_error">An error occurred accessing the camera</string>
|
||||
<string name="video_creating_error">An error occurred creating the video file</string>
|
||||
|
@ -13,6 +15,15 @@
|
|||
<string name="setting_resolution_failed">Setting proper resolution failed</string>
|
||||
<string name="video_recording_failed">Video recording failed, try using a different resolution</string>
|
||||
|
||||
<!--TODO: Add strings in other locales-->
|
||||
<string name="camera_in_use_error">Camera is in use by another app, please close the app and try again</string>
|
||||
<string name="camera_configure_error">An error occurred while configuring the camera</string>
|
||||
<string name="camera_disabled_by_admin_error">Camera is disabled by the admin</string>
|
||||
<string name="camera_dnd_error">"Do Not Disturb" mode is enabled. Please disable and try again</string>
|
||||
|
||||
<string name="photo_capture_failed">Photo capture failed</string>
|
||||
<string name="video_capture_insufficient_storage_error">Video recording failed due to insufficient storage</string>
|
||||
|
||||
<!-- FAQ -->
|
||||
<string name="faq_1_title">What photo compression quality should I set?</string>
|
||||
<string name="faq_1_text">It depends on your goal. For generic purposes most people advise using 75%-80%, when the image is still really good quality, but the file size is reduced drastically compared to 100%.</string>
|
||||
|
|
Loading…
Reference in New Issue