From 074351b88fc99467139178a0bc7113b84c46e51f Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 15:43:39 +0100 Subject: [PATCH 01/11] 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 --- app/build.gradle | 14 +- .../camera/activities/MainActivity.kt | 130 +++--- .../camera/extensions/Int.kt | 25 ++ .../camera/implementations/CameraXPreview.kt | 394 ++++++++++++++++++ .../implementations/CameraXPreviewListener.kt | 13 + .../camera/interfaces/MyPreview.kt | 17 +- .../camera/views/CameraPreview.kt | 15 +- app/src/main/res/layout/activity_main.xml | 8 +- 8 files changed, 541 insertions(+), 75 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt diff --git a/app/build.gradle b/app/build.gradle index 4042216e..e1e07cea 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt index adebc9d9..83fff326 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -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 { diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt new file mode 100644 index 00000000..d4b445ab --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt @@ -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") + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt new file mode 100644 index 00000000..a2d9a138 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -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? = 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 { + 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" + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt new file mode 100644 index 00000000..c404a855 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt @@ -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) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt index f1437148..232b936f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt @@ -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 } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/views/CameraPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/views/CameraPreview.kt index 5ffea60d..98ca1710 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/views/CameraPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/views/CameraPreview.kt @@ -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() { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 71ec8765..94059da6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,10 +5,10 @@ android:layout_height="match_parent" android:background="@android:color/black"> - + Date: Sat, 25 Jun 2022 16:39:29 +0100 Subject: [PATCH 02/11] handle playing media sounds - add MediaSoundHelper to separate logic for playing media action sounds - add support for playing media action sounds (shutter, start and stop recording) in CameraXPreview --- .../camera/activities/MainActivity.kt | 19 +++++++ .../camera/helpers/MediaSoundHelper.kt | 25 ++++++++++ .../camera/implementations/CameraXPreview.kt | 49 ++++++++++++++++--- .../implementations/CameraXPreviewListener.kt | 3 ++ .../camera/views/CameraPreview.kt | 16 ++---- 5 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt index 83fff326..fa6bc420 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -39,6 +39,7 @@ 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 java.util.concurrent.TimeUnit 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 @@ -549,6 +550,24 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera updateFlashlightState(flashMode) } + override fun onVideoRecordingStarted() { + shutter.setImageResource(R.drawable.ic_video_stop) + toggle_camera.beInvisible() + video_rec_curr_timer.beVisible() + } + + override fun onVideoRecordingStopped() { + shutter.setImageResource(R.drawable.ic_video_rec) + video_rec_curr_timer.text = 0.getFormattedDuration() + video_rec_curr_timer.beGone() + toggle_camera.beVisible() + } + + override fun onVideoDurationChanged(durationNanos: Long) { + val seconds = TimeUnit.NANOSECONDS.toSeconds(durationNanos).toInt() + video_rec_curr_timer.text = seconds.getFormattedDuration() + } + fun setRecordingState(isRecording: Boolean) { runOnUiThread { if (isRecording) { diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt new file mode 100644 index 00000000..abc2e71a --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt @@ -0,0 +1,25 @@ +package com.simplemobiletools.camera.helpers + +import android.media.MediaActionSound + +class MediaSoundHelper { + private val mediaActionSound = MediaActionSound() + + fun loadSounds() { + mediaActionSound.load(MediaActionSound.START_VIDEO_RECORDING) + mediaActionSound.load(MediaActionSound.STOP_VIDEO_RECORDING) + mediaActionSound.load(MediaActionSound.SHUTTER_CLICK) + } + + fun playShutterSound() { + mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) + } + + fun playStartVideoRecordingSound() { + mediaActionSound.play(MediaActionSound.START_VIDEO_RECORDING) + } + + fun playStopVideoRecordingSound() { + mediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index a2d9a138..6c306beb 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -37,6 +37,7 @@ 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.helpers.MediaSoundHelper import com.simplemobiletools.camera.interfaces.MyPreview import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.extensions.toast @@ -63,6 +64,7 @@ class CameraXPreview( private val config = activity.config private val contentResolver = activity.contentResolver private val mainExecutor = activity.mainExecutor + private val mediaSoundHelper = MediaSoundHelper() private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { @SuppressLint("RestrictedApi") @@ -109,6 +111,7 @@ class CameraXPreview( init { bindToLifeCycle() + mediaSoundHelper.loadSounds() viewFinder.doOnLayout { startCamera() } @@ -315,6 +318,7 @@ class CameraXPreview( .setMetadata(metadata) .build() + imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback { override fun onImageSaved(outputFileResults: OutputFileResults) { listener.toggleBottomButtons(false) @@ -327,6 +331,7 @@ class CameraXPreview( Log.e(TAG, "Error", exception) } }) + playShutterSoundIfEnabled() } override fun initPhotoMode() { @@ -368,15 +373,25 @@ class CameraXPreview( .withAudioEnabled() .start(mainExecutor) { recordEvent -> Log.d(TAG, "recordEvent=$recordEvent ") - if (recordEvent !is VideoRecordEvent.Status) { - recordingState = recordEvent - } + recordingState = recordEvent + when(recordEvent){ + is VideoRecordEvent.Start -> { + playStartVideoRecordingSoundIfEnabled() + listener.onVideoRecordingStarted() + } - if (recordEvent is VideoRecordEvent.Finalize) { - if (recordEvent.hasError()) { - // TODO: Handle errors - } else { - listener.onMediaCaptured(recordEvent.outputResults.outputUri) + is VideoRecordEvent.Status -> { + listener.onVideoDurationChanged(recordEvent.recordingStats.recordedDurationNanos) + } + + is VideoRecordEvent.Finalize -> { + playStopVideoRecordingSoundIfEnabled() + listener.onVideoRecordingStopped() + if (recordEvent.hasError()) { + // TODO: Handle errors + } else { + listener.onMediaCaptured(recordEvent.outputResults.outputUri) + } } } } @@ -391,4 +406,22 @@ class CameraXPreview( "VID_$timestamp" } } + + private fun playShutterSoundIfEnabled(){ + if(config.isSoundEnabled){ + mediaSoundHelper.playShutterSound() + } + } + + private fun playStartVideoRecordingSoundIfEnabled(){ + if(config.isSoundEnabled){ + mediaSoundHelper.playStartVideoRecordingSound() + } + } + + private fun playStopVideoRecordingSoundIfEnabled(){ + if(config.isSoundEnabled){ + mediaSoundHelper.playStopVideoRecordingSound() + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt index c404a855..934c6710 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt @@ -10,4 +10,7 @@ interface CameraXPreviewListener { fun toggleBottomButtons(hide:Boolean) fun onMediaCaptured(uri: Uri) fun onChangeFlashMode(flashMode: Int) + fun onVideoRecordingStarted() + fun onVideoRecordingStopped() + fun onVideoDurationChanged(durationNanos: Long) } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/views/CameraPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/views/CameraPreview.kt index 98ca1710..adf039ed 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/views/CameraPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/views/CameraPreview.kt @@ -96,7 +96,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview { private val mCameraToPreviewMatrix = Matrix() private val mPreviewToCameraMatrix = Matrix() private val mCameraOpenCloseLock = Semaphore(1) - private val mMediaActionSound = MediaActionSound() + private val mediaSoundHelper = MediaSoundHelper() private var mZoomRect: Rect? = null constructor(context: Context) : super(context) @@ -114,7 +114,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview { mUseFrontCamera = false mIsInVideoMode = !initPhotoMode - loadSounds() + mediaSoundHelper.loadSounds() val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { @@ -182,12 +182,6 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview { } } - private fun loadSounds() { - mMediaActionSound.load(MediaActionSound.START_VIDEO_RECORDING) - mMediaActionSound.load(MediaActionSound.STOP_VIDEO_RECORDING) - mMediaActionSound.load(MediaActionSound.SHUTTER_CLICK) - } - @SuppressLint("MissingPermission") private fun openCamera(width: Int, height: Int) { try { @@ -576,7 +570,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview { } if (mActivity.config.isSoundEnabled) { - mMediaActionSound.play(MediaActionSound.SHUTTER_CLICK) + mediaSoundHelper.playShutterSound() } mCameraState = STATE_PICTURE_TAKEN @@ -824,7 +818,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview { closeCaptureSession() setupMediaRecorder() if (mActivity.config.isSoundEnabled) { - mMediaActionSound.play(MediaActionSound.START_VIDEO_RECORDING) + mediaSoundHelper.playStartVideoRecordingSound() } try { @@ -853,7 +847,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview { private fun stopRecording() { mCameraState = STATE_STOPING_RECORDING if (mActivity.config.isSoundEnabled) { - mMediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING) + mediaSoundHelper.playStopVideoRecordingSound() } mIsRecording = false From 1f3dd341d0dafe3b0359b287f087f83e8e120126 Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 17:14:49 +0100 Subject: [PATCH 03/11] add flipping horizontally and photo quality --- .../simplemobiletools/camera/implementations/CameraXPreview.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index 6c306beb..730508f9 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -233,6 +233,7 @@ class CameraXPreview( return ImageCapture.Builder() .setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY) .setFlashMode(flashMode) + .setJpegQuality(config.photoQuality) .setTargetAspectRatio(aspectRatio) .setTargetRotation(rotation) .build() @@ -305,7 +306,7 @@ class CameraXPreview( val imageCapture = imageCapture ?: throw IllegalStateException("Camera initialization failed.") val metadata = Metadata().apply { - isReversedHorizontal = frontCameraInUse + isReversedHorizontal = config.flipPhotos } val contentValues = ContentValues().apply { From f0030670cff709812a043cf36c241df17c9231a8 Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 17:51:13 +0100 Subject: [PATCH 04/11] add basic support for focus and metering --- .../camera/activities/MainActivity.kt | 4 ++ .../camera/implementations/CameraXPreview.kt | 50 ++++++++++++++++--- .../implementations/CameraXPreviewListener.kt | 1 + 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt index fa6bc420..5f701f58 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -568,6 +568,10 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera video_rec_curr_timer.text = seconds.getFormattedDuration() } + override fun onFocusCamera(xPos: Float, yPos: Float) { + mFocusCircleView.drawFocusCircle(xPos, yPos) + } + fun setRecordingState(isRecording: Boolean) { runOnUiThread { if (isRecording) { diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index 730508f9..52817444 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -7,6 +7,9 @@ import android.net.Uri import android.os.Environment import android.provider.MediaStore import android.util.Log +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.MotionEvent import android.view.OrientationEventListener import android.view.Surface import androidx.appcompat.app.AppCompatActivity @@ -152,6 +155,7 @@ class CameraXPreview( captureUseCase, ) preview?.setSurfaceProvider(viewFinder.surfaceProvider) + setupFocus() } private fun setupCameraObservers() { @@ -261,6 +265,38 @@ class CameraXPreview( return AspectRatio.RATIO_16_9 } + @SuppressLint("ClickableViewAccessibility") + // source: https://stackoverflow.com/a/60095886/10552591 + private fun setupFocus() { + val gestureDetector = GestureDetector(activity, object : SimpleOnGestureListener() { + override fun onSingleTapConfirmed(event: MotionEvent?): Boolean { + Log.i(TAG, "onSingleTapConfirmed: x=${event?.x}, y=${event?.y}") + return event?.let { + val factory: MeteringPointFactory = SurfaceOrientedMeteringPointFactory(viewFinder.width.toFloat(), viewFinder.height.toFloat()) + val xPos = event.x + val yPos = event.y + val autoFocusPoint = factory.createPoint(event.x, event.y) + try { + val focusMeteringAction = FocusMeteringAction.Builder(autoFocusPoint, FocusMeteringAction.FLAG_AF) + .disableAutoCancel() + .build() + camera?.cameraControl?.startFocusAndMetering(focusMeteringAction) + listener.onFocusCamera(xPos, yPos) + Log.i(TAG, "start focus") + } catch (e: CameraInfoUnavailableException) { + Log.e(TAG, "cannot access camera", e) + } + true + } ?: false + } + }) + viewFinder.setOnTouchListener { _, event -> + Log.i(TAG, "setOnTouchListener: x=${event.x}, y=${event.y}") + gestureDetector.onTouchEvent(event) + true + } + } + override fun onStart(owner: LifecycleOwner) { orientationEventListener.enable() } @@ -375,7 +411,7 @@ class CameraXPreview( .start(mainExecutor) { recordEvent -> Log.d(TAG, "recordEvent=$recordEvent ") recordingState = recordEvent - when(recordEvent){ + when (recordEvent) { is VideoRecordEvent.Start -> { playStartVideoRecordingSoundIfEnabled() listener.onVideoRecordingStarted() @@ -408,20 +444,20 @@ class CameraXPreview( } } - private fun playShutterSoundIfEnabled(){ - if(config.isSoundEnabled){ + private fun playShutterSoundIfEnabled() { + if (config.isSoundEnabled) { mediaSoundHelper.playShutterSound() } } - private fun playStartVideoRecordingSoundIfEnabled(){ - if(config.isSoundEnabled){ + private fun playStartVideoRecordingSoundIfEnabled() { + if (config.isSoundEnabled) { mediaSoundHelper.playStartVideoRecordingSound() } } - private fun playStopVideoRecordingSoundIfEnabled(){ - if(config.isSoundEnabled){ + private fun playStopVideoRecordingSoundIfEnabled() { + if (config.isSoundEnabled) { mediaSoundHelper.playStopVideoRecordingSound() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt index 934c6710..b5758137 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt @@ -13,4 +13,5 @@ interface CameraXPreviewListener { fun onVideoRecordingStarted() fun onVideoRecordingStopped() fun onVideoDurationChanged(durationNanos: Long) + fun onFocusCamera(xPos: Float, yPos: Float) } From e15078499b8003c063e7aa65a11c192e97b0b0f7 Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 18:10:33 +0100 Subject: [PATCH 05/11] modify package name and app name - this is so we can install both versions on the same device - it would be reverted when camerax is ready to go live --- app/build.gradle | 2 +- app/src/debug/res/values/strings.xml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e1e07cea..71d85744 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,7 +34,7 @@ android { buildTypes { debug { - applicationIdSuffix ".debug" + applicationIdSuffix ".debugcamerax" } release { minifyEnabled true diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index 37ff84c6..e7e9ee39 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -1,4 +1,5 @@ - Camera_debug + + CameraX_debug From b44b6a9b2b01479bee76953e11535cdd1dc9ae49 Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 21:42:15 +0100 Subject: [PATCH 06/11] update focus and unbind useCase - update to use the DisplayOrientedMeteringPointFactory - add Auto Focus (AF) and Auto Exposure (AE) metering points - unbind VideoCaptureUseCase when in photo mode - unbind ImageCaptureUseCase when not in photo mode --- .../camera/implementations/CameraXPreview.kt | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index 52817444..da933c64 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -2,11 +2,14 @@ package com.simplemobiletools.camera.implementations import android.annotation.SuppressLint import android.content.ContentValues +import android.content.Context import android.hardware.SensorManager +import android.hardware.display.DisplayManager import android.net.Uri import android.os.Environment import android.provider.MediaStore import android.util.Log +import android.view.Display import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent @@ -44,7 +47,6 @@ import com.simplemobiletools.camera.helpers.MediaSoundHelper 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 @@ -54,7 +56,7 @@ import kotlin.math.min class CameraXPreview( private val activity: AppCompatActivity, - private val viewFinder: PreviewView, + private val previewView: PreviewView, private val listener: CameraXPreviewListener, ) : MyPreview, DefaultLifecycleObserver { @@ -62,13 +64,19 @@ class CameraXPreview( 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 + + // Auto focus is 1/6 of the area. + private const val AF_SIZE = 1.0f / 6.0f + private const val AE_SIZE = AF_SIZE * 1.5f } private val config = activity.config private val contentResolver = activity.contentResolver private val mainExecutor = activity.mainExecutor + private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val mediaSoundHelper = MediaSoundHelper() private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() + private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { @SuppressLint("RestrictedApi") override fun onOrientationChanged(orientation: Int) { @@ -115,7 +123,7 @@ class CameraXPreview( init { bindToLifeCycle() mediaSoundHelper.loadSounds() - viewFinder.doOnLayout { + previewView.doOnLayout { startCamera() } } @@ -143,7 +151,7 @@ class CameraXPreview( 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 + val rotation = previewView.display.rotation preview = buildPreview(aspectRatio, rotation) val captureUseCase = getCaptureUseCase(aspectRatio, rotation) @@ -154,8 +162,8 @@ class CameraXPreview( preview, captureUseCase, ) - preview?.setSurfaceProvider(viewFinder.surfaceProvider) - setupFocus() + preview?.setSurfaceProvider(previewView.surfaceProvider) + setupZoomAndFocus() } private fun setupCameraObservers() { @@ -223,10 +231,12 @@ class CameraXPreview( private fun getCaptureUseCase(aspectRatio: Int, rotation: Int): UseCase { return if (isPhotoCapture) { + cameraProvider?.unbind(videoCapture) buildImageCapture(aspectRatio, rotation).also { imageCapture = it } } else { + cameraProvider?.unbind(imageCapture) buildVideoCapture().also { videoCapture = it } @@ -252,6 +262,7 @@ class CameraXPreview( private fun buildVideoCapture(): VideoCapture { val recorder = Recorder.Builder() + //TODO: user control for quality .setQualitySelector(QualitySelector.from(Quality.FHD)) .build() return VideoCapture.withOutput(recorder) @@ -267,30 +278,32 @@ class CameraXPreview( @SuppressLint("ClickableViewAccessibility") // source: https://stackoverflow.com/a/60095886/10552591 - private fun setupFocus() { + private fun setupZoomAndFocus() { + Log.i(TAG, "camera controller: ${previewView.controller}") val gestureDetector = GestureDetector(activity, object : SimpleOnGestureListener() { - override fun onSingleTapConfirmed(event: MotionEvent?): Boolean { - Log.i(TAG, "onSingleTapConfirmed: x=${event?.x}, y=${event?.y}") - return event?.let { - val factory: MeteringPointFactory = SurfaceOrientedMeteringPointFactory(viewFinder.width.toFloat(), viewFinder.height.toFloat()) + override fun onSingleTapConfirmed(event: MotionEvent): Boolean { + return camera?.cameraInfo?.let { + val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) + val width = previewView.width.toFloat() + val height = previewView.height.toFloat() + Log.i(TAG, "onSingleTapConfirmed: width=$width,height=$height") + val factory = DisplayOrientedMeteringPointFactory(display, it, width, height) val xPos = event.x val yPos = event.y - val autoFocusPoint = factory.createPoint(event.x, event.y) - try { - val focusMeteringAction = FocusMeteringAction.Builder(autoFocusPoint, FocusMeteringAction.FLAG_AF) - .disableAutoCancel() - .build() - camera?.cameraControl?.startFocusAndMetering(focusMeteringAction) - listener.onFocusCamera(xPos, yPos) - Log.i(TAG, "start focus") - } catch (e: CameraInfoUnavailableException) { - Log.e(TAG, "cannot access camera", e) - } + val autoFocusPoint = factory.createPoint(xPos, yPos, AF_SIZE) + val autoExposurePoint = factory.createPoint(xPos, yPos, AE_SIZE) + val focusMeteringAction = FocusMeteringAction.Builder(autoFocusPoint, FocusMeteringAction.FLAG_AF) + .addPoint(autoExposurePoint, FocusMeteringAction.FLAG_AE) + .disableAutoCancel() + .build() + camera?.cameraControl?.startFocusAndMetering(focusMeteringAction) + listener.onFocusCamera(xPos, yPos) + Log.i(TAG, "start focus") true } ?: false } }) - viewFinder.setOnTouchListener { _, event -> + previewView.setOnTouchListener { _, event -> Log.i(TAG, "setOnTouchListener: x=${event.x}, y=${event.y}") gestureDetector.onTouchEvent(event) true From fe3710510de5cbb5d72e1165d48eb5b06a4530fe Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 22:42:47 +0100 Subject: [PATCH 07/11] add pinch to zoom support --- .../PinchToZoomOnScaleGestureListener.kt | 19 ++++++++++++++++++ .../camera/helpers/ZoomCalculator.kt | 20 +++++++++++++++++++ .../camera/implementations/CameraXPreview.kt | 5 +++++ 3 files changed, 44 insertions(+) create mode 100644 app/src/main/kotlin/com/simplemobiletools/camera/helpers/PinchToZoomOnScaleGestureListener.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/camera/helpers/ZoomCalculator.kt diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/PinchToZoomOnScaleGestureListener.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/PinchToZoomOnScaleGestureListener.kt new file mode 100644 index 00000000..3289e433 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/PinchToZoomOnScaleGestureListener.kt @@ -0,0 +1,19 @@ +package com.simplemobiletools.camera.helpers + +import android.view.ScaleGestureDetector +import androidx.camera.core.CameraControl +import androidx.camera.core.CameraInfo + +class PinchToZoomOnScaleGestureListener( + private val cameraInfo: CameraInfo, + private val cameraControl: CameraControl, +) : ScaleGestureDetector.SimpleOnScaleGestureListener() { + private val zoomCalculator = ZoomCalculator() + + override fun onScale(detector: ScaleGestureDetector): Boolean { + val zoomState = cameraInfo.zoomState.value ?: return false + val zoomRatio = zoomCalculator.calculateZoomRatio(zoomState, detector.scaleFactor) + cameraControl.setZoomRatio(zoomRatio) + return true + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/ZoomCalculator.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/ZoomCalculator.kt new file mode 100644 index 00000000..1e2aa294 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/ZoomCalculator.kt @@ -0,0 +1,20 @@ +package com.simplemobiletools.camera.helpers + +import androidx.camera.core.ZoomState + +class ZoomCalculator { + + fun calculateZoomRatio(zoomState: ZoomState, pinchToZoomScale: Float): Float { + val clampedRatio = zoomState.zoomRatio * speedUpZoomBy2X(pinchToZoomScale) + // Clamp the ratio with the zoom range. + return clampedRatio.coerceAtLeast(zoomState.minZoomRatio).coerceAtMost(zoomState.maxZoomRatio) + } + + private fun speedUpZoomBy2X(scaleFactor: Float): Float { + return if (scaleFactor > 1f) { + 1.0f + (scaleFactor - 1.0f) * 2 + } else { + 1.0f - (1.0f - scaleFactor) * 2 + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index da933c64..e3c23c16 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -14,6 +14,7 @@ import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent import android.view.OrientationEventListener +import android.view.ScaleGestureDetector import android.view.Surface import androidx.appcompat.app.AppCompatActivity import androidx.camera.core.* @@ -44,6 +45,8 @@ import com.simplemobiletools.camera.extensions.config import com.simplemobiletools.camera.extensions.toAppFlashMode import com.simplemobiletools.camera.extensions.toCameraXFlashMode import com.simplemobiletools.camera.helpers.MediaSoundHelper +import com.simplemobiletools.camera.helpers.PinchToZoomOnScaleGestureListener +import com.simplemobiletools.camera.helpers.ZoomCalculator import com.simplemobiletools.camera.interfaces.MyPreview import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.extensions.toast @@ -280,6 +283,7 @@ class CameraXPreview( // source: https://stackoverflow.com/a/60095886/10552591 private fun setupZoomAndFocus() { Log.i(TAG, "camera controller: ${previewView.controller}") + val scaleGesture = camera?.let { ScaleGestureDetector(activity, PinchToZoomOnScaleGestureListener(it.cameraInfo, it.cameraControl)) } val gestureDetector = GestureDetector(activity, object : SimpleOnGestureListener() { override fun onSingleTapConfirmed(event: MotionEvent): Boolean { return camera?.cameraInfo?.let { @@ -306,6 +310,7 @@ class CameraXPreview( previewView.setOnTouchListener { _, event -> Log.i(TAG, "setOnTouchListener: x=${event.x}, y=${event.y}") gestureDetector.onTouchEvent(event) + scaleGesture?.onTouchEvent(event) true } } From 916121ffc92dc645c7d5afaa671a0e3c309e7e5f Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 23:21:51 +0100 Subject: [PATCH 08/11] store/restore last used camera lens --- .../camera/extensions/CameraSelector.kt | 11 +++++++++++ .../com/simplemobiletools/camera/extensions/Int.kt | 10 ++++++++++ .../com/simplemobiletools/camera/helpers/Config.kt | 5 +++++ .../simplemobiletools/camera/helpers/Constants.kt | 1 + .../camera/implementations/CameraXPreview.kt | 13 ++++++++----- 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/camera/extensions/CameraSelector.kt diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/extensions/CameraSelector.kt b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/CameraSelector.kt new file mode 100644 index 00000000..0b98bca4 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/CameraSelector.kt @@ -0,0 +1,11 @@ +package com.simplemobiletools.camera.extensions + +import androidx.camera.core.CameraSelector + +fun CameraSelector.toLensFacing(): Int { + return if (this == CameraSelector.DEFAULT_FRONT_CAMERA) { + CameraSelector.LENS_FACING_FRONT + } else { + CameraSelector.LENS_FACING_BACK + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt index d4b445ab..4cf4caf7 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt @@ -1,5 +1,6 @@ package com.simplemobiletools.camera.extensions +import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture import com.simplemobiletools.camera.helpers.FLASH_AUTO import com.simplemobiletools.camera.helpers.FLASH_OFF @@ -23,3 +24,12 @@ fun Int.toAppFlashMode(): Int { else -> throw IllegalArgumentException("Unknown mode: $this") } } + +fun Int.toCameraSelector(): CameraSelector { + return if (this == CameraSelector.LENS_FACING_FRONT) { + CameraSelector.DEFAULT_FRONT_CAMERA + } else { + CameraSelector.DEFAULT_BACK_CAMERA + } +} + diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt index ca317988..5ede2c12 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt @@ -2,6 +2,7 @@ package com.simplemobiletools.camera.helpers import android.content.Context import android.os.Environment +import androidx.camera.core.CameraSelector import com.simplemobiletools.commons.helpers.BaseConfig import java.io.File @@ -37,6 +38,10 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getString(LAST_USED_CAMERA, "0")!! set(cameraId) = prefs.edit().putString(LAST_USED_CAMERA, cameraId).apply() + var lastUsedCameraLens: Int + get() = prefs.getInt(LAST_USED_CAMERA_LENS, CameraSelector.LENS_FACING_BACK) + set(lens) = prefs.edit().putInt(LAST_USED_CAMERA_LENS, lens).apply() + var initPhotoMode: Boolean get() = prefs.getBoolean(INIT_PHOTO_MODE, true) set(initPhotoMode) = prefs.edit().putBoolean(INIT_PHOTO_MODE, initPhotoMode).apply() diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Constants.kt index b68dff71..0b543b7c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Constants.kt @@ -10,6 +10,7 @@ const val SOUND = "sound" const val VOLUME_BUTTONS_AS_SHUTTER = "volume_buttons_as_shutter" const val FLIP_PHOTOS = "flip_photos" const val LAST_USED_CAMERA = "last_used_camera_2" +const val LAST_USED_CAMERA_LENS = "last_used_camera_lens" const val FLASHLIGHT_STATE = "flashlight_state" const val INIT_PHOTO_MODE = "init_photo_mode" const val BACK_PHOTO_RESOLUTION_INDEX = "back_photo_resolution_index_2" diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index e3c23c16..7504c8e5 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -43,10 +43,11 @@ 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.toCameraSelector import com.simplemobiletools.camera.extensions.toCameraXFlashMode +import com.simplemobiletools.camera.extensions.toLensFacing import com.simplemobiletools.camera.helpers.MediaSoundHelper import com.simplemobiletools.camera.helpers.PinchToZoomOnScaleGestureListener -import com.simplemobiletools.camera.helpers.ZoomCalculator import com.simplemobiletools.camera.interfaces.MyPreview import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.extensions.toast @@ -110,7 +111,7 @@ class CameraXPreview( get() = cameraProvider?.availableCameraInfos?.size ?: 0 private val frontCameraInUse: Boolean - get() = lensFacing == CameraSelector.DEFAULT_FRONT_CAMERA + get() = cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA private var preview: Preview? = null private var cameraProvider: ProcessCameraProvider? = null @@ -119,7 +120,7 @@ class CameraXPreview( private var camera: Camera? = null private var currentRecording: Recording? = null private var recordingState: VideoRecordEvent? = null - private var lensFacing = CameraSelector.DEFAULT_BACK_CAMERA + private var cameraSelector = config.lastUsedCameraLens.toCameraSelector() private var flashMode = config.flashlightState.toCameraXFlashMode() private var isPhotoCapture = config.initPhotoMode @@ -161,7 +162,7 @@ class CameraXPreview( cameraProvider.unbindAll() camera = cameraProvider.bindToLifecycle( activity, - lensFacing, + cameraSelector, preview, captureUseCase, ) @@ -332,11 +333,13 @@ class CameraXPreview( } override fun toggleFrontBackCamera() { - lensFacing = if (frontCameraInUse) { + val newCameraSelector = if (frontCameraInUse) { CameraSelector.DEFAULT_BACK_CAMERA } else { CameraSelector.DEFAULT_FRONT_CAMERA } + cameraSelector = newCameraSelector + config.lastUsedCameraLens = newCameraSelector.toLensFacing() startCamera() } From cc5fa462c4a2e1ad2126868fd79e7781d492041e Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 23:22:37 +0100 Subject: [PATCH 09/11] revert compileSdkVersion and targetSdkVersion to 31 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 71d85744..e9c65e21 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,12 +9,12 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 32 + compileSdkVersion 31 defaultConfig { applicationId "com.simplemobiletools.camera" minSdkVersion 29 - targetSdkVersion 32 + targetSdkVersion 31 versionCode 77 versionName "5.3.1" setProperty("archivesBaseName", "camera") From d168040722135a95c9a0c826d1be62790ce31794 Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 23:25:20 +0100 Subject: [PATCH 10/11] replace property getters with actual functions in CameraXPreview --- .../camera/implementations/CameraXPreview.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index 7504c8e5..580acecf 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -101,18 +101,6 @@ class CameraXPreview( } } - 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() = cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA - private var preview: Preview? = null private var cameraProvider: ProcessCameraProvider? = null private var imageCapture: ImageCapture? = null @@ -172,13 +160,13 @@ class CameraXPreview( private fun setupCameraObservers() { listener.setFlashAvailable(camera?.cameraInfo?.hasFlashUnit() ?: false) - listener.onChangeCamera(frontCameraInUse) + listener.onChangeCamera(isFrontCameraInUse()) camera?.cameraInfo?.cameraState?.observe(activity) { cameraState -> when (cameraState.type) { CameraState.Type.OPEN, CameraState.Type.OPENING -> { - listener.setHasFrontAndBackCamera(hasFrontCamera && hasBackCamera) + listener.setHasFrontAndBackCamera(hasFrontCamera() && hasBackCamera()) listener.setCameraAvailable(true) } CameraState.Type.PENDING_OPEN, @@ -280,6 +268,18 @@ class CameraXPreview( return AspectRatio.RATIO_16_9 } + private fun hasBackCamera(): Boolean { + return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false + } + + private fun hasFrontCamera(): Boolean { + return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false + } + + private fun isFrontCameraInUse(): Boolean { + return cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA + } + @SuppressLint("ClickableViewAccessibility") // source: https://stackoverflow.com/a/60095886/10552591 private fun setupZoomAndFocus() { @@ -333,7 +333,7 @@ class CameraXPreview( } override fun toggleFrontBackCamera() { - val newCameraSelector = if (frontCameraInUse) { + val newCameraSelector = if (isFrontCameraInUse()) { CameraSelector.DEFAULT_BACK_CAMERA } else { CameraSelector.DEFAULT_FRONT_CAMERA From e691fc1e8c334c6928658926b125c7a655c02bef Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sun, 26 Jun 2022 00:12:19 +0100 Subject: [PATCH 11/11] use more stable CameraX 1.1.0-rc02 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 37c76c58..fbc8af34 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,7 +68,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1" implementation 'androidx.window:window:1.1.0-alpha02' - def camerax_version = '1.2.0-alpha02' + def camerax_version = '1.1.0-rc02' implementation "androidx.camera:camera-core:$camerax_version" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-video:$camerax_version"