From 074351b88fc99467139178a0bc7113b84c46e51f Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 25 Jun 2022 15:43:39 +0100 Subject: [PATCH] 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"> - +