mirror of
				https://github.com/SimpleMobileTools/Simple-Camera.git
				synced 2025-06-27 09:02:59 +02:00 
			
		
		
		
	add initial camera-x implementation
- add CameraXPreview - basic support image capture - basic support for video capture - add CameraXPreviewListener to prevent coupling to MainActivity - support switching camera, flash light modes - modify MyPreview interface to add default implementation for methods not needed by the CameraXPreview
This commit is contained in:
		| @@ -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" | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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") | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,394 @@ | ||||
| package com.simplemobiletools.camera.implementations | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.ContentValues | ||||
| import android.hardware.SensorManager | ||||
| import android.net.Uri | ||||
| import android.os.Environment | ||||
| import android.provider.MediaStore | ||||
| import android.util.Log | ||||
| import android.view.OrientationEventListener | ||||
| import android.view.Surface | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.camera.core.* | ||||
| import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY | ||||
| import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO | ||||
| import androidx.camera.core.ImageCapture.FLASH_MODE_OFF | ||||
| import androidx.camera.core.ImageCapture.FLASH_MODE_ON | ||||
| import androidx.camera.core.ImageCapture.Metadata | ||||
| import androidx.camera.core.ImageCapture.OnImageSavedCallback | ||||
| import androidx.camera.core.ImageCapture.OutputFileOptions | ||||
| import androidx.camera.core.ImageCapture.OutputFileResults | ||||
| import androidx.camera.lifecycle.ProcessCameraProvider | ||||
| import androidx.camera.video.MediaStoreOutputOptions | ||||
| import androidx.camera.video.Quality | ||||
| import androidx.camera.video.QualitySelector | ||||
| import androidx.camera.video.Recorder | ||||
| import androidx.camera.video.Recording | ||||
| import androidx.camera.video.VideoCapture | ||||
| import androidx.camera.video.VideoRecordEvent | ||||
| import androidx.camera.view.PreviewView | ||||
| import androidx.core.view.doOnLayout | ||||
| import androidx.lifecycle.DefaultLifecycleObserver | ||||
| import androidx.lifecycle.LifecycleOwner | ||||
| import androidx.window.layout.WindowMetricsCalculator | ||||
| import com.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION | ||||
| import com.simplemobiletools.camera.R | ||||
| import com.simplemobiletools.camera.extensions.config | ||||
| import com.simplemobiletools.camera.extensions.toAppFlashMode | ||||
| import com.simplemobiletools.camera.extensions.toCameraXFlashMode | ||||
| import com.simplemobiletools.camera.interfaces.MyPreview | ||||
| import com.simplemobiletools.commons.extensions.showErrorToast | ||||
| import com.simplemobiletools.commons.extensions.toast | ||||
| import java.lang.IllegalArgumentException | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Date | ||||
| import java.util.Locale | ||||
| import kotlin.math.abs | ||||
| import kotlin.math.max | ||||
| import kotlin.math.min | ||||
|  | ||||
| class CameraXPreview( | ||||
|     private val activity: AppCompatActivity, | ||||
|     private val viewFinder: PreviewView, | ||||
|     private val listener: CameraXPreviewListener, | ||||
| ) : MyPreview, DefaultLifecycleObserver { | ||||
|  | ||||
|     companion object { | ||||
|         private const val TAG = "CameraXPreview" | ||||
|         private const val RATIO_4_3_VALUE = 4.0 / 3.0 | ||||
|         private const val RATIO_16_9_VALUE = 16.0 / 9.0 | ||||
|     } | ||||
|  | ||||
|     private val config = activity.config | ||||
|     private val contentResolver = activity.contentResolver | ||||
|     private val mainExecutor = activity.mainExecutor | ||||
|     private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() | ||||
|     private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { | ||||
|         @SuppressLint("RestrictedApi") | ||||
|         override fun onOrientationChanged(orientation: Int) { | ||||
|             if (orientation == UNKNOWN_ORIENTATION) { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             val rotation = when (orientation) { | ||||
|                 in 45 until 135 -> Surface.ROTATION_270 | ||||
|                 in 135 until 225 -> Surface.ROTATION_180 | ||||
|                 in 225 until 315 -> Surface.ROTATION_90 | ||||
|                 else -> Surface.ROTATION_0 | ||||
|             } | ||||
|  | ||||
|             preview?.targetRotation = rotation | ||||
|             imageCapture?.targetRotation = rotation | ||||
|             videoCapture?.targetRotation = rotation | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private val hasBackCamera: Boolean | ||||
|         get() = cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false | ||||
|  | ||||
|     private val hasFrontCamera: Boolean | ||||
|         get() = cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false | ||||
|  | ||||
|     private val cameraCount: Int | ||||
|         get() = cameraProvider?.availableCameraInfos?.size ?: 0 | ||||
|  | ||||
|     private val frontCameraInUse: Boolean | ||||
|         get() = lensFacing == CameraSelector.DEFAULT_FRONT_CAMERA | ||||
|  | ||||
|     private var preview: Preview? = null | ||||
|     private var cameraProvider: ProcessCameraProvider? = null | ||||
|     private var imageCapture: ImageCapture? = null | ||||
|     private var videoCapture: VideoCapture<Recorder>? = null | ||||
|     private var camera: Camera? = null | ||||
|     private var currentRecording: Recording? = null | ||||
|     private var recordingState: VideoRecordEvent? = null | ||||
|     private var lensFacing = CameraSelector.DEFAULT_BACK_CAMERA | ||||
|     private var flashMode = config.flashlightState.toCameraXFlashMode() | ||||
|     private var isPhotoCapture = config.initPhotoMode | ||||
|  | ||||
|     init { | ||||
|         bindToLifeCycle() | ||||
|         viewFinder.doOnLayout { | ||||
|             startCamera() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun bindToLifeCycle() { | ||||
|         activity.lifecycle.addObserver(this) | ||||
|     } | ||||
|  | ||||
|     private fun startCamera() { | ||||
|         Log.i(TAG, "startCamera: ") | ||||
|         val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) | ||||
|         cameraProviderFuture.addListener({ | ||||
|             try { | ||||
|                 cameraProvider = cameraProviderFuture.get() | ||||
|                 bindCameraUseCases() | ||||
|                 setupCameraObservers() | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "startCamera: ", e) | ||||
|                 activity.showErrorToast(activity.getString(R.string.camera_open_error)) | ||||
|             } | ||||
|         }, mainExecutor) | ||||
|     } | ||||
|  | ||||
|     private fun bindCameraUseCases() { | ||||
|         val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.") | ||||
|         val metrics = windowMetricsCalculator.computeCurrentWindowMetrics(activity).bounds | ||||
|         val aspectRatio = aspectRatio(metrics.width(), metrics.height()) | ||||
|         val rotation = viewFinder.display.rotation | ||||
|  | ||||
|         preview = buildPreview(aspectRatio, rotation) | ||||
|         val captureUseCase = getCaptureUseCase(aspectRatio, rotation) | ||||
|         cameraProvider.unbindAll() | ||||
|         camera = cameraProvider.bindToLifecycle( | ||||
|             activity, | ||||
|             lensFacing, | ||||
|             preview, | ||||
|             captureUseCase, | ||||
|         ) | ||||
|         preview?.setSurfaceProvider(viewFinder.surfaceProvider) | ||||
|     } | ||||
|  | ||||
|     private fun setupCameraObservers() { | ||||
|         listener.setFlashAvailable(camera?.cameraInfo?.hasFlashUnit() ?: false) | ||||
|         listener.onChangeCamera(frontCameraInUse) | ||||
|  | ||||
|         camera?.cameraInfo?.cameraState?.observe(activity) { cameraState -> | ||||
|             when (cameraState.type) { | ||||
|                 CameraState.Type.OPEN, | ||||
|                 CameraState.Type.OPENING -> { | ||||
|                     listener.setHasFrontAndBackCamera(hasFrontCamera && hasBackCamera) | ||||
|                     listener.setCameraAvailable(true) | ||||
|                 } | ||||
|                 CameraState.Type.PENDING_OPEN, | ||||
|                 CameraState.Type.CLOSING, | ||||
|                 CameraState.Type.CLOSED -> { | ||||
|                     listener.setCameraAvailable(false) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // TODO: Handle errors | ||||
|             cameraState.error?.let { error -> | ||||
|                 listener.setCameraAvailable(false) | ||||
|                 when (error.code) { | ||||
|                     CameraState.ERROR_STREAM_CONFIG -> { | ||||
|                         Log.e(TAG, "ERROR_STREAM_CONFIG") | ||||
|                         // Make sure to setup the use cases properly | ||||
|                         activity.toast(R.string.camera_unavailable) | ||||
|                     } | ||||
|                     CameraState.ERROR_CAMERA_IN_USE -> { | ||||
|                         Log.e(TAG, "ERROR_CAMERA_IN_USE") | ||||
|                         // Close the camera or ask user to close another camera app that's using the | ||||
|                         // camera | ||||
|                         activity.showErrorToast("Camera is in use by another app, please close") | ||||
|                     } | ||||
|                     CameraState.ERROR_MAX_CAMERAS_IN_USE -> { | ||||
|                         Log.e(TAG, "ERROR_MAX_CAMERAS_IN_USE") | ||||
|                         // Close another open camera in the app, or ask the user to close another | ||||
|                         // camera app that's using the camera | ||||
|                         activity.showErrorToast("Camera is in use by another app, please close") | ||||
|                     } | ||||
|                     CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> { | ||||
|                         Log.e(TAG, "ERROR_OTHER_RECOVERABLE_ERROR") | ||||
|                         activity.toast(R.string.camera_open_error) | ||||
|                     } | ||||
|                     CameraState.ERROR_CAMERA_DISABLED -> { | ||||
|                         Log.e(TAG, "ERROR_CAMERA_DISABLED") | ||||
|                         // Ask the user to enable the device's cameras | ||||
|                         activity.toast(R.string.camera_open_error) | ||||
|                     } | ||||
|                     CameraState.ERROR_CAMERA_FATAL_ERROR -> { | ||||
|                         Log.e(TAG, "ERROR_CAMERA_FATAL_ERROR") | ||||
|                         // Ask the user to reboot the device to restore camera function | ||||
|                         activity.toast(R.string.camera_open_error) | ||||
|                     } | ||||
|                     CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> { | ||||
|                         // Ask the user to disable the "Do Not Disturb" mode, then reopen the camera | ||||
|                         Log.e(TAG, "ERROR_DO_NOT_DISTURB_MODE_ENABLED") | ||||
|                         activity.toast(R.string.camera_open_error) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getCaptureUseCase(aspectRatio: Int, rotation: Int): UseCase { | ||||
|         return if (isPhotoCapture) { | ||||
|             buildImageCapture(aspectRatio, rotation).also { | ||||
|                 imageCapture = it | ||||
|             } | ||||
|         } else { | ||||
|             buildVideoCapture().also { | ||||
|                 videoCapture = it | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun buildImageCapture(aspectRatio: Int, rotation: Int): ImageCapture { | ||||
|         return ImageCapture.Builder() | ||||
|             .setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY) | ||||
|             .setFlashMode(flashMode) | ||||
|             .setTargetAspectRatio(aspectRatio) | ||||
|             .setTargetRotation(rotation) | ||||
|             .build() | ||||
|     } | ||||
|  | ||||
|     private fun buildPreview(aspectRatio: Int, rotation: Int): Preview { | ||||
|         return Preview.Builder() | ||||
|             .setTargetAspectRatio(aspectRatio) | ||||
|             .setTargetRotation(rotation) | ||||
|             .build() | ||||
|     } | ||||
|  | ||||
|     private fun buildVideoCapture(): VideoCapture<Recorder> { | ||||
|         val recorder = Recorder.Builder() | ||||
|             .setQualitySelector(QualitySelector.from(Quality.FHD)) | ||||
|             .build() | ||||
|         return VideoCapture.withOutput(recorder) | ||||
|     } | ||||
|  | ||||
|     private fun aspectRatio(width: Int, height: Int): Int { | ||||
|         val previewRatio = max(width, height).toDouble() / min(width, height) | ||||
|         if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) { | ||||
|             return AspectRatio.RATIO_4_3 | ||||
|         } | ||||
|         return AspectRatio.RATIO_16_9 | ||||
|     } | ||||
|  | ||||
|     override fun onStart(owner: LifecycleOwner) { | ||||
|         orientationEventListener.enable() | ||||
|     } | ||||
|  | ||||
|     override fun onStop(owner: LifecycleOwner) { | ||||
|         orientationEventListener.disable() | ||||
|     } | ||||
|  | ||||
|     override fun setTargetUri(uri: Uri) { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     override fun showChangeResolutionDialog() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     override fun toggleFrontBackCamera() { | ||||
|         lensFacing = if (frontCameraInUse) { | ||||
|             CameraSelector.DEFAULT_BACK_CAMERA | ||||
|         } else { | ||||
|             CameraSelector.DEFAULT_FRONT_CAMERA | ||||
|         } | ||||
|         startCamera() | ||||
|     } | ||||
|  | ||||
|     override fun toggleFlashlight() { | ||||
|         val newFlashMode = when (flashMode) { | ||||
|             FLASH_MODE_OFF -> FLASH_MODE_ON | ||||
|             FLASH_MODE_ON -> FLASH_MODE_AUTO | ||||
|             FLASH_MODE_AUTO -> FLASH_MODE_OFF | ||||
|             else -> throw IllegalArgumentException("Unknown mode: $flashMode") | ||||
|         } | ||||
|  | ||||
|         flashMode = newFlashMode | ||||
|         imageCapture?.flashMode = newFlashMode | ||||
|         val appFlashMode = flashMode.toAppFlashMode() | ||||
|         config.flashlightState = appFlashMode | ||||
|         listener.onChangeFlashMode(appFlashMode) | ||||
|     } | ||||
|  | ||||
|     override fun tryTakePicture() { | ||||
|         Log.i(TAG, "captureImage: ") | ||||
|         val imageCapture = imageCapture ?: throw IllegalStateException("Camera initialization failed.") | ||||
|  | ||||
|         val metadata = Metadata().apply { | ||||
|             isReversedHorizontal = frontCameraInUse | ||||
|         } | ||||
|  | ||||
|         val contentValues = ContentValues().apply { | ||||
|             put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(true)) | ||||
|             put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") | ||||
|             put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM) | ||||
|         } | ||||
|         val contentUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) | ||||
|         val outputOptions = OutputFileOptions.Builder(contentResolver, contentUri, contentValues) | ||||
|             .setMetadata(metadata) | ||||
|             .build() | ||||
|  | ||||
|         imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback { | ||||
|             override fun onImageSaved(outputFileResults: OutputFileResults) { | ||||
|                 listener.toggleBottomButtons(false) | ||||
|                 listener.onMediaCaptured(outputFileResults.savedUri!!) | ||||
|             } | ||||
|  | ||||
|             override fun onError(exception: ImageCaptureException) { | ||||
|                 listener.toggleBottomButtons(false) | ||||
|                 activity.showErrorToast("Capture picture $exception") | ||||
|                 Log.e(TAG, "Error", exception) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     override fun initPhotoMode() { | ||||
|         isPhotoCapture = true | ||||
|         startCamera() | ||||
|     } | ||||
|  | ||||
|     override fun initVideoMode() { | ||||
|         isPhotoCapture = false | ||||
|         startCamera() | ||||
|     } | ||||
|  | ||||
|     override fun toggleRecording() { | ||||
|         Log.d(TAG, "toggleRecording: currentRecording=$currentRecording, recordingState=$recordingState") | ||||
|         if (currentRecording == null || recordingState is VideoRecordEvent.Finalize) { | ||||
|             startRecording() | ||||
|         } else { | ||||
|             currentRecording?.stop() | ||||
|             currentRecording = null | ||||
|             Log.d(TAG, "Recording stopped") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("MissingPermission") | ||||
|     private fun startRecording() { | ||||
|         val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.") | ||||
|         val contentValues = ContentValues().apply { | ||||
|             put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(false)) | ||||
|             put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4") | ||||
|             put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM) | ||||
|         } | ||||
|         val contentUri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) | ||||
|         val outputOptions = MediaStoreOutputOptions.Builder(contentResolver, contentUri) | ||||
|             .setContentValues(contentValues) | ||||
|             .build() | ||||
|  | ||||
|         currentRecording = videoCapture.output | ||||
|             .prepareRecording(activity, outputOptions) | ||||
|             .withAudioEnabled() | ||||
|             .start(mainExecutor) { recordEvent -> | ||||
|                 Log.d(TAG, "recordEvent=$recordEvent ") | ||||
|                 if (recordEvent !is VideoRecordEvent.Status) { | ||||
|                     recordingState = recordEvent | ||||
|                 } | ||||
|  | ||||
|                 if (recordEvent is VideoRecordEvent.Finalize) { | ||||
|                     if (recordEvent.hasError()) { | ||||
|                         // TODO: Handle errors | ||||
|                     } else { | ||||
|                         listener.onMediaCaptured(recordEvent.outputResults.outputUri) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         Log.d(TAG, "Recording started") | ||||
|     } | ||||
|  | ||||
|     private fun getRandomMediaName(isPhoto: Boolean): String { | ||||
|         val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) | ||||
|         return if (isPhoto) { | ||||
|             "IMG_$timestamp" | ||||
|         } else { | ||||
|             "VID_$timestamp" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -5,10 +5,10 @@ | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="@android:color/black"> | ||||
|  | ||||
|     <com.simplemobiletools.camera.views.AutoFitTextureView | ||||
|         android:id="@+id/camera_texture_view" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" /> | ||||
|     <androidx.camera.view.PreviewView | ||||
|         android:id="@+id/view_finder" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" /> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/capture_black_screen" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user