add initial camera-x implementation

- add CameraXPreview
  - basic support image capture
  - basic support for video capture
  - add CameraXPreviewListener to prevent coupling to MainActivity
  - support switching camera, flash light modes
- modify MyPreview interface to add default implementation for methods not needed by the CameraXPreview
This commit is contained in:
darthpaul 2022-06-25 15:43:39 +01:00
parent 0cdf08b45f
commit 074351b88f
8 changed files with 541 additions and 75 deletions

View File

@ -9,12 +9,12 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 31
compileSdkVersion 32
defaultConfig {
applicationId "com.simplemobiletools.camera"
minSdkVersion 29
targetSdkVersion 31
targetSdkVersion 32
versionCode 77
versionName "5.3.1"
setProperty("archivesBaseName", "camera")
@ -65,4 +65,14 @@ dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:d5e1100f27'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.exifinterface:exifinterface:1.3.3"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
implementation 'androidx.window:window:1.1.0-alpha02'
def camerax_version = '1.2.0-alpha02'
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-video:$camerax_version"
implementation "androidx.camera:camera-extensions:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
}

View File

@ -7,7 +7,12 @@ import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.provider.MediaStore
import android.view.*
import android.util.Log
import android.view.KeyEvent
import android.view.OrientationEventListener
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.RelativeLayout
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -16,17 +21,39 @@ import com.bumptech.glide.request.RequestOptions
import com.simplemobiletools.camera.BuildConfig
import com.simplemobiletools.camera.R
import com.simplemobiletools.camera.extensions.config
import com.simplemobiletools.camera.helpers.*
import com.simplemobiletools.camera.helpers.FLASH_OFF
import com.simplemobiletools.camera.helpers.FLASH_ON
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.CameraXPreviewListener
import com.simplemobiletools.camera.implementations.MyCameraImpl
import com.simplemobiletools.camera.interfaces.MyPreview
import com.simplemobiletools.camera.views.CameraPreview
import com.simplemobiletools.camera.views.FocusCircleView
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.helpers.BROADCAST_REFRESH_MEDIA
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.models.Release
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main.btn_holder
import kotlinx.android.synthetic.main.activity_main.capture_black_screen
import kotlinx.android.synthetic.main.activity_main.change_resolution
import kotlinx.android.synthetic.main.activity_main.last_photo_video_preview
import kotlinx.android.synthetic.main.activity_main.settings
import kotlinx.android.synthetic.main.activity_main.shutter
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.view_holder
class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener {
private val TAG = "MainActivity"
private val FADE_DELAY = 5000L
private val CAPTURE_ANIMATION_DURATION = 100L
@ -68,7 +95,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
override fun onResume() {
super.onResume()
if (hasStorageAndCameraPermissions()) {
mPreview?.onResumed()
resumeCameraItems()
setupPreviewImage(mIsInPhotoMode)
scheduleFadeOut()
@ -97,14 +123,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
hideTimer()
mOrientationEventListener.disable()
if (mPreview?.getCameraState() == STATE_PICTURE_TAKEN) {
toast(R.string.photo_not_saved)
}
ensureBackgroundThread {
mPreview?.onPaused()
}
}
override fun onDestroy() {
@ -201,8 +219,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
)
checkVideoCaptureIntent()
mPreview = CameraPreview(this, camera_texture_view, mIsInPhotoMode)
view_holder.addView(mPreview as ViewGroup)
mPreview = CameraXPreview(this, view_finder, this)
checkImageCaptureIntent()
mPreview?.setIsImageCaptureIntent(isImageCaptureIntent())
@ -217,7 +234,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
mFadeHandler = Handler()
setupPreviewImage(true)
val initialFlashlightState = FLASH_OFF
val initialFlashlightState = config.flashlightState
mPreview!!.setFlashlightState(initialFlashlightState)
updateFlashlightState(initialFlashlightState)
}
@ -261,10 +278,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
toggle_flash.setImageResource(flashDrawable)
}
fun updateCameraIcon(isUsingFrontCamera: Boolean) {
toggle_camera.setImageResource(if (isUsingFrontCamera) R.drawable.ic_camera_rear_vector else R.drawable.ic_camera_front_vector)
}
private fun shutterPressed() {
if (checkCameraAvailable()) {
handleShutter()
@ -283,19 +296,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
}
}
fun toggleBottomButtons(hide: Boolean) {
runOnUiThread {
val alpha = if (hide) 0f else 1f
shutter.animate().alpha(alpha).start()
toggle_camera.animate().alpha(alpha).start()
toggle_flash.animate().alpha(alpha).start()
shutter.isClickable = !hide
toggle_camera.isClickable = !hide
toggle_flash.isClickable = !hide
}
}
private fun launchSettings() {
if (settings.alpha == 1f) {
val intent = Intent(applicationContext, SettingsActivity::class.java)
@ -324,14 +324,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
}
if (mIsVideoCaptureIntent) {
mPreview?.tryInitVideoMode()
mPreview?.initVideoMode()
}
mPreview?.setFlashlightState(FLASH_OFF)
hideTimer()
mIsInPhotoMode = !mIsInPhotoMode
config.initPhotoMode = mIsInPhotoMode
showToggleCameraIfNeeded()
checkButtons()
toggleBottomButtons(false)
}
@ -352,9 +351,10 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
}
private fun tryInitVideoMode() {
if (mPreview?.initVideoMode() == true) {
try {
mPreview?.initVideoMode()
initVideoButtons()
} else {
} catch (e: Exception) {
if (!mIsVideoCaptureIntent) {
toast(R.string.video_mode_error)
}
@ -363,7 +363,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
private fun initVideoButtons() {
toggle_photo_video.setImageResource(R.drawable.ic_camera_vector)
showToggleCameraIfNeeded()
shutter.setImageResource(R.drawable.ic_video_rec)
setupPreviewImage(false)
mPreview?.checkFlashlight()
@ -378,6 +377,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
mPreviewUri = Uri.withAppendedPath(uri, lastMediaId.toString())
Log.e(TAG, "mPreviewUri= $mPreviewUri")
loadLastTakenMedia(mPreviewUri)
}
private fun loadLastTakenMedia(uri: Uri?) {
mPreviewUri = uri
runOnUiThread {
if (!isDestroyed) {
val options = RequestOptions()
@ -385,7 +391,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
.diskCacheStrategy(DiskCacheStrategy.NONE)
Glide.with(this)
.load(mPreviewUri)
.load(uri)
.apply(options)
.transition(DrawableTransitionOptions.withCrossFade())
.into(last_photo_video_preview)
@ -447,7 +453,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
}
private fun resumeCameraItems() {
showToggleCameraIfNeeded()
hideNavigationBarIcons()
if (!mIsInPhotoMode) {
@ -455,10 +460,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
}
}
private fun showToggleCameraIfNeeded() {
toggle_camera?.beInvisibleIf(mCameraImpl.getCountOfCameras() ?: 1 <= 1)
}
private fun hasStorageAndCameraPermissions() = hasPermission(PERMISSION_WRITE_STORAGE) && hasPermission(PERMISSION_CAMERA)
private fun setupOrientationEventListener() {
@ -505,7 +506,15 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
return mIsCameraAvailable
}
fun setFlashAvailable(available: Boolean) {
override fun setCameraAvailable(available: Boolean) {
mIsCameraAvailable = available
}
override fun setHasFrontAndBackCamera(hasFrontAndBack: Boolean) {
toggle_camera?.beVisibleIf(hasFrontAndBack)
}
override fun setFlashAvailable(available: Boolean) {
if (available) {
toggle_flash.beVisible()
} else {
@ -515,8 +524,29 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
}
}
fun setIsCameraAvailable(available: Boolean) {
mIsCameraAvailable = available
override fun onChangeCamera(frontCamera: Boolean) {
toggle_camera.setImageResource(if (frontCamera) R.drawable.ic_camera_rear_vector else R.drawable.ic_camera_front_vector)
}
override fun toggleBottomButtons(hide: Boolean) {
runOnUiThread {
val alpha = if (hide) 0f else 1f
shutter.animate().alpha(alpha).start()
toggle_camera.animate().alpha(alpha).start()
toggle_flash.animate().alpha(alpha).start()
shutter.isClickable = !hide
toggle_camera.isClickable = !hide
toggle_flash.isClickable = !hide
}
}
override fun onMediaCaptured(uri: Uri) {
loadLastTakenMedia(uri)
}
override fun onChangeFlashMode(flashMode: Int) {
updateFlashlightState(flashMode)
}
fun setRecordingState(isRecording: Boolean) {
@ -527,7 +557,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
showTimer()
} else {
shutter.setImageResource(R.drawable.ic_video_rec)
showToggleCameraIfNeeded()
hideTimer()
}
}
@ -548,6 +577,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
fun drawFocusCircle(x: Float, y: Float) = mFocusCircleView.drawFocusCircle(x, y)
override fun mediaSaved(path: String) {
Log.e(TAG, "mediaSaved: $path")
rescanPaths(arrayListOf(path)) {
setupPreviewImage(true)
Intent(BROADCAST_REFRESH_MEDIA).apply {

View File

@ -0,0 +1,25 @@
package com.simplemobiletools.camera.extensions
import androidx.camera.core.ImageCapture
import com.simplemobiletools.camera.helpers.FLASH_AUTO
import com.simplemobiletools.camera.helpers.FLASH_OFF
import com.simplemobiletools.camera.helpers.FLASH_ON
import java.lang.IllegalArgumentException
fun Int.toCameraXFlashMode(): Int {
return when (this) {
FLASH_ON -> ImageCapture.FLASH_MODE_ON
FLASH_OFF -> ImageCapture.FLASH_MODE_OFF
FLASH_AUTO -> ImageCapture.FLASH_MODE_AUTO
else -> throw IllegalArgumentException("Unknown mode: $this")
}
}
fun Int.toAppFlashMode(): Int {
return when (this) {
ImageCapture.FLASH_MODE_ON -> FLASH_ON
ImageCapture.FLASH_MODE_OFF -> FLASH_OFF
ImageCapture.FLASH_MODE_AUTO -> FLASH_AUTO
else -> throw IllegalArgumentException("Unknown mode: $this")
}
}

View File

@ -0,0 +1,394 @@
package com.simplemobiletools.camera.implementations
import android.annotation.SuppressLint
import android.content.ContentValues
import android.hardware.SensorManager
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.view.OrientationEventListener
import android.view.Surface
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
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
import androidx.camera.core.ImageCapture.FLASH_MODE_ON
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.lifecycle.ProcessCameraProvider
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.camera.view.PreviewView
import androidx.core.view.doOnLayout
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
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.toAppFlashMode
import com.simplemobiletools.camera.extensions.toCameraXFlashMode
import com.simplemobiletools.camera.interfaces.MyPreview
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.extensions.toast
import java.lang.IllegalArgumentException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class CameraXPreview(
private val activity: AppCompatActivity,
private val viewFinder: PreviewView,
private val listener: CameraXPreviewListener,
) : MyPreview, DefaultLifecycleObserver {
companion object {
private const val TAG = "CameraXPreview"
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
}
private val config = activity.config
private val contentResolver = activity.contentResolver
private val mainExecutor = activity.mainExecutor
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) {
@SuppressLint("RestrictedApi")
override fun onOrientationChanged(orientation: Int) {
if (orientation == UNKNOWN_ORIENTATION) {
return
}
val rotation = when (orientation) {
in 45 until 135 -> Surface.ROTATION_270
in 135 until 225 -> Surface.ROTATION_180
in 225 until 315 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}
preview?.targetRotation = rotation
imageCapture?.targetRotation = rotation
videoCapture?.targetRotation = rotation
}
}
private val hasBackCamera: Boolean
get() = cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
private val hasFrontCamera: Boolean
get() = cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
private val cameraCount: Int
get() = cameraProvider?.availableCameraInfos?.size ?: 0
private val frontCameraInUse: Boolean
get() = lensFacing == CameraSelector.DEFAULT_FRONT_CAMERA
private var preview: Preview? = null
private var cameraProvider: ProcessCameraProvider? = null
private var imageCapture: ImageCapture? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var camera: Camera? = null
private var currentRecording: Recording? = null
private var recordingState: VideoRecordEvent? = null
private var lensFacing = CameraSelector.DEFAULT_BACK_CAMERA
private var flashMode = config.flashlightState.toCameraXFlashMode()
private var isPhotoCapture = config.initPhotoMode
init {
bindToLifeCycle()
viewFinder.doOnLayout {
startCamera()
}
}
private fun bindToLifeCycle() {
activity.lifecycle.addObserver(this)
}
private fun startCamera() {
Log.i(TAG, "startCamera: ")
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
cameraProviderFuture.addListener({
try {
cameraProvider = cameraProviderFuture.get()
bindCameraUseCases()
setupCameraObservers()
} catch (e: Exception) {
Log.e(TAG, "startCamera: ", e)
activity.showErrorToast(activity.getString(R.string.camera_open_error))
}
}, mainExecutor)
}
private fun bindCameraUseCases() {
val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.")
val metrics = windowMetricsCalculator.computeCurrentWindowMetrics(activity).bounds
val aspectRatio = aspectRatio(metrics.width(), metrics.height())
val rotation = viewFinder.display.rotation
preview = buildPreview(aspectRatio, rotation)
val captureUseCase = getCaptureUseCase(aspectRatio, rotation)
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(
activity,
lensFacing,
preview,
captureUseCase,
)
preview?.setSurfaceProvider(viewFinder.surfaceProvider)
}
private fun setupCameraObservers() {
listener.setFlashAvailable(camera?.cameraInfo?.hasFlashUnit() ?: false)
listener.onChangeCamera(frontCameraInUse)
camera?.cameraInfo?.cameraState?.observe(activity) { cameraState ->
when (cameraState.type) {
CameraState.Type.OPEN,
CameraState.Type.OPENING -> {
listener.setHasFrontAndBackCamera(hasFrontCamera && hasBackCamera)
listener.setCameraAvailable(true)
}
CameraState.Type.PENDING_OPEN,
CameraState.Type.CLOSING,
CameraState.Type.CLOSED -> {
listener.setCameraAvailable(false)
}
}
// 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)
}
}
}
}
}
private fun getCaptureUseCase(aspectRatio: Int, rotation: Int): UseCase {
return if (isPhotoCapture) {
buildImageCapture(aspectRatio, rotation).also {
imageCapture = it
}
} else {
buildVideoCapture().also {
videoCapture = it
}
}
}
private fun buildImageCapture(aspectRatio: Int, rotation: Int): ImageCapture {
return ImageCapture.Builder()
.setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
.setFlashMode(flashMode)
.setTargetAspectRatio(aspectRatio)
.setTargetRotation(rotation)
.build()
}
private fun buildPreview(aspectRatio: Int, rotation: Int): Preview {
return Preview.Builder()
.setTargetAspectRatio(aspectRatio)
.setTargetRotation(rotation)
.build()
}
private fun buildVideoCapture(): VideoCapture<Recorder> {
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.FHD))
.build()
return VideoCapture.withOutput(recorder)
}
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = max(width, height).toDouble() / min(width, height)
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
override fun onStart(owner: LifecycleOwner) {
orientationEventListener.enable()
}
override fun onStop(owner: LifecycleOwner) {
orientationEventListener.disable()
}
override fun setTargetUri(uri: Uri) {
}
override fun showChangeResolutionDialog() {
}
override fun toggleFrontBackCamera() {
lensFacing = if (frontCameraInUse) {
CameraSelector.DEFAULT_BACK_CAMERA
} else {
CameraSelector.DEFAULT_FRONT_CAMERA
}
startCamera()
}
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")
}
flashMode = newFlashMode
imageCapture?.flashMode = newFlashMode
val appFlashMode = flashMode.toAppFlashMode()
config.flashlightState = appFlashMode
listener.onChangeFlashMode(appFlashMode)
}
override fun tryTakePicture() {
Log.i(TAG, "captureImage: ")
val imageCapture = imageCapture ?: throw IllegalStateException("Camera initialization failed.")
val metadata = Metadata().apply {
isReversedHorizontal = frontCameraInUse
}
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)
val outputOptions = OutputFileOptions.Builder(contentResolver, contentUri, contentValues)
.setMetadata(metadata)
.build()
imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback {
override fun onImageSaved(outputFileResults: OutputFileResults) {
listener.toggleBottomButtons(false)
listener.onMediaCaptured(outputFileResults.savedUri!!)
}
override fun onError(exception: ImageCaptureException) {
listener.toggleBottomButtons(false)
activity.showErrorToast("Capture picture $exception")
Log.e(TAG, "Error", exception)
}
})
}
override fun initPhotoMode() {
isPhotoCapture = true
startCamera()
}
override fun initVideoMode() {
isPhotoCapture = false
startCamera()
}
override fun toggleRecording() {
Log.d(TAG, "toggleRecording: currentRecording=$currentRecording, recordingState=$recordingState")
if (currentRecording == null || recordingState is VideoRecordEvent.Finalize) {
startRecording()
} else {
currentRecording?.stop()
currentRecording = null
Log.d(TAG, "Recording stopped")
}
}
@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()
.start(mainExecutor) { recordEvent ->
Log.d(TAG, "recordEvent=$recordEvent ")
if (recordEvent !is VideoRecordEvent.Status) {
recordingState = recordEvent
}
if (recordEvent is VideoRecordEvent.Finalize) {
if (recordEvent.hasError()) {
// TODO: Handle errors
} else {
listener.onMediaCaptured(recordEvent.outputResults.outputUri)
}
}
}
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"
}
}
}

View File

@ -0,0 +1,13 @@
package com.simplemobiletools.camera.implementations
import android.net.Uri
interface CameraXPreviewListener {
fun setCameraAvailable(available: Boolean)
fun setHasFrontAndBackCamera(hasFrontAndBack:Boolean)
fun setFlashAvailable(available: Boolean)
fun onChangeCamera(frontCamera: Boolean)
fun toggleBottomButtons(hide:Boolean)
fun onMediaCaptured(uri: Uri)
fun onChangeFlashMode(flashMode: Int)
}

View File

@ -3,17 +3,18 @@ package com.simplemobiletools.camera.interfaces
import android.net.Uri
interface MyPreview {
fun onResumed()
fun onPaused()
fun onResumed() = Unit
fun onPaused() = Unit
fun setTargetUri(uri: Uri)
fun setIsImageCaptureIntent(isImageCaptureIntent: Boolean)
fun setIsImageCaptureIntent(isImageCaptureIntent: Boolean) = Unit
fun setFlashlightState(state: Int)
fun setFlashlightState(state: Int) = Unit
fun getCameraState(): Int
fun getCameraState(): Int = 0
fun showChangeResolutionDialog()
@ -25,11 +26,9 @@ interface MyPreview {
fun toggleRecording()
fun tryInitVideoMode()
fun initPhotoMode()
fun initVideoMode(): Boolean
fun initVideoMode()
fun checkFlashlight()
fun checkFlashlight() = Unit
}

View File

@ -369,7 +369,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
mIsFocusSupported = get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)!!.size > 1
}
mActivity.setFlashAvailable(mIsFlashSupported)
mActivity.updateCameraIcon(mUseFrontCamera)
mActivity.onChangeCamera(mUseFrontCamera)
return
}
} catch (e: Exception) {
@ -429,21 +429,21 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
mCameraOpenCloseLock.release()
mCameraDevice = cameraDevice
createCameraPreviewSession()
mActivity.setIsCameraAvailable(true)
mActivity.setCameraAvailable(true)
}
override fun onDisconnected(cameraDevice: CameraDevice) {
mCameraOpenCloseLock.release()
cameraDevice.close()
mCameraDevice = null
mActivity.setIsCameraAvailable(false)
mActivity.setCameraAvailable(false)
}
override fun onError(cameraDevice: CameraDevice, error: Int) {
mCameraOpenCloseLock.release()
cameraDevice.close()
mCameraDevice = null
mActivity.setIsCameraAvailable(false)
mActivity.setCameraAvailable(false)
}
}
@ -981,23 +981,18 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
}
}
override fun tryInitVideoMode() {
initVideoMode()
}
override fun initPhotoMode() {
mIsInVideoMode = false
closeCamera()
openCamera(mTextureView.width, mTextureView.height)
}
override fun initVideoMode(): Boolean {
override fun initVideoMode() {
mLastFocusX = 0f
mLastFocusY = 0f
mIsInVideoMode = true
closeCamera()
openCamera(mTextureView.width, mTextureView.height)
return true
}
override fun checkFlashlight() {

View File

@ -5,10 +5,10 @@
android:layout_height="match_parent"
android:background="@android:color/black">
<com.simplemobiletools.camera.views.AutoFitTextureView
android:id="@+id/camera_texture_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<androidx.camera.view.PreviewView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/capture_black_screen"