Files
Simple-Camera/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt
2023-07-24 21:06:29 +03:00

653 lines
25 KiB
Kotlin

package com.simplemobiletools.camera.implementations
import android.annotation.SuppressLint
import android.content.Context
import android.hardware.SensorManager
import android.hardware.display.DisplayManager
import android.os.Handler
import android.os.Looper
import android.util.Rational
import android.util.Size
import android.view.*
import android.view.GestureDetector.SimpleOnGestureListener
import androidx.camera.core.*
import androidx.camera.core.ImageCapture.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.*
import androidx.camera.video.VideoCapture
import androidx.camera.view.PreviewView
import androidx.camera.view.PreviewView.ScaleType
import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
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.*
import com.simplemobiletools.camera.helpers.*
import com.simplemobiletools.camera.interfaces.MyPreview
import com.simplemobiletools.camera.models.CaptureMode
import com.simplemobiletools.camera.models.MediaOutput
import com.simplemobiletools.camera.models.MySize
import com.simplemobiletools.camera.models.ResolutionOption
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
class CameraXPreview(
private val activity: BaseSimpleActivity,
private val previewView: PreviewView,
private val mediaSoundHelper: MediaSoundHelper,
private val mediaOutputHelper: MediaOutputHelper,
private val cameraErrorHandler: CameraErrorHandler,
private val listener: CameraXPreviewListener,
private val isThirdPartyIntent: Boolean,
initInPhotoMode: Boolean,
) : MyPreview, DefaultLifecycleObserver {
companion object {
// 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 const val CAMERA_MODE_SWITCH_WAIT_TIME = 500L
}
private val config = activity.config
private val contentResolver = activity.contentResolver
private val mainExecutor = ContextCompat.getMainExecutor(activity)
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
private val videoQualityManager = VideoQualityManager(activity)
private val imageQualityManager = ImageQualityManager(activity)
private val mediaSizeStore = MediaSizeStore(config)
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
}
if (lastRotation != rotation) {
preview?.targetRotation = rotation
imageCapture?.targetRotation = rotation
videoCapture?.targetRotation = rotation
lastRotation = rotation
}
}
}
private val cameraHandler = Handler(Looper.getMainLooper())
private val photoModeRunnable = Runnable {
if (imageCapture == null) {
isPhotoCapture = true
if (!isThirdPartyIntent) { // we don't want to store the state for 3rd party intents
config.initPhotoMode = true
}
startCamera()
} else {
listener.onInitPhotoMode()
}
}
private val videoModeRunnable = Runnable {
if (videoCapture == null) {
isPhotoCapture = false
if (!isThirdPartyIntent) { // we don't want to store the state for 3rd party intents
config.initPhotoMode = false
}
startCamera()
} else {
listener.onInitVideoMode()
}
}
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 cameraSelector = config.lastUsedCameraLens.toCameraSelector()
private var flashMode = FLASH_MODE_OFF
private var isPhotoCapture = initInPhotoMode
private var lastRotation = 0
private var lastCameraStartTime = 0L
private var simpleLocationManager: SimpleLocationManager? = null
init {
bindToLifeCycle()
}
private fun bindToLifeCycle() {
activity.lifecycle.addObserver(this)
}
private fun startCamera(switching: Boolean = false) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity.applicationContext)
cameraProviderFuture.addListener({
try {
val provider = cameraProviderFuture.get()
cameraProvider = provider
imageQualityManager.initSupportedQualities()
videoQualityManager.initSupportedQualities(provider)
bindCameraUseCases()
setupCameraObservers()
} catch (e: Exception) {
val errorMessage = if (switching) R.string.camera_switch_error else R.string.camera_open_error
activity.toast(errorMessage)
}
}, mainExecutor)
}
private fun bindCameraUseCases() {
val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.")
val resolution = if (isPhotoCapture) {
imageQualityManager.getUserSelectedResolution(cameraSelector).also {
listener.displaySelectedResolution(it.toResolutionOption())
}
} else {
val selectedQuality = videoQualityManager.getUserSelectedQuality(cameraSelector).also {
listener.displaySelectedResolution(it.toResolutionOption())
}
MySize(selectedQuality.width, selectedQuality.height)
}
listener.adjustPreviewView(resolution.requiresCentering())
val isFullSize = resolution.isFullScreen
previewView.scaleType = if (isFullSize) ScaleType.FILL_CENTER else ScaleType.FIT_CENTER
val rotation = previewView.display.rotation
val rotatedResolution = getRotatedResolution(resolution, rotation)
val previewUseCase = buildPreview(rotatedResolution, rotation)
val captureUseCase = getCaptureUseCase(rotatedResolution, rotation)
cameraProvider.unbindAll()
camera = if (isFullSize) {
val metrics = windowMetricsCalculator.computeCurrentWindowMetrics(activity).bounds
val screenWidth = metrics.width()
val screenHeight = metrics.height()
val viewPort = ViewPort.Builder(Rational(screenWidth, screenHeight), rotation).build()
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(previewUseCase)
.addUseCase(captureUseCase)
.setViewPort(viewPort)
.build()
cameraProvider.bindToLifecycle(
activity,
cameraSelector,
useCaseGroup,
)
} else {
cameraProvider.bindToLifecycle(
activity,
cameraSelector,
previewUseCase,
captureUseCase,
)
}
preview = previewUseCase
setupZoomAndFocus()
setFlashlightState(config.flashlightState)
}
private fun getRotatedResolution(resolution: MySize, rotationDegrees: Int): Size {
return if (rotationDegrees == Surface.ROTATION_0 || rotationDegrees == Surface.ROTATION_180) {
Size(resolution.height, resolution.width)
} else {
Size(resolution.width, resolution.height)
}
}
private fun buildPreview(resolution: Size, rotation: Int): Preview {
return Preview.Builder()
.setTargetRotation(rotation)
.setTargetResolution(resolution)
.build().apply {
setSurfaceProvider(previewView.surfaceProvider)
}
}
private fun getCaptureUseCase(resolution: Size, rotation: Int): UseCase {
return if (isPhotoCapture) {
buildImageCapture(resolution, rotation).also {
imageCapture = it
videoCapture = null
}
} else {
buildVideoCapture().also {
videoCapture = it
imageCapture = null
}
}
}
private fun buildImageCapture(resolution: Size, rotation: Int): ImageCapture {
return Builder()
.setCaptureMode(getCaptureMode())
.setFlashMode(flashMode)
.setJpegQuality(config.photoQuality)
.setTargetRotation(rotation)
.setTargetResolution(resolution)
.build()
}
private fun getCaptureMode(): Int {
return when (config.captureMode) {
CaptureMode.MINIMIZE_LATENCY -> CAPTURE_MODE_MINIMIZE_LATENCY
CaptureMode.MAXIMIZE_QUALITY -> CAPTURE_MODE_MAXIMIZE_QUALITY
}
}
private fun buildVideoCapture(): VideoCapture<Recorder> {
val qualitySelector = QualitySelector.from(
videoQualityManager.getUserSelectedQuality(cameraSelector).toCameraXQuality(),
FallbackStrategy.higherQualityOrLowerThan(Quality.SD),
)
val recorder = Recorder.Builder()
.setQualitySelector(qualitySelector)
.build()
return VideoCapture.withOutput(recorder)
}
private fun setupCameraObservers() {
listener.setFlashAvailable(camera?.cameraInfo?.hasFlashUnit() ?: false)
listener.onChangeCamera(isFrontCameraInUse())
if (isPhotoCapture) {
listener.onInitPhotoMode()
} else {
listener.onInitVideoMode()
}
camera?.cameraInfo?.cameraState?.observe(activity) { cameraState ->
if (cameraState.error == null) {
when (cameraState.type) {
CameraState.Type.OPENING,
CameraState.Type.OPEN -> {
listener.setHasFrontAndBackCamera(hasFrontCamera() && hasBackCamera())
listener.setCameraAvailable(true)
}
CameraState.Type.PENDING_OPEN,
CameraState.Type.CLOSING,
CameraState.Type.CLOSED -> {
listener.setCameraAvailable(false)
}
}
} else {
listener.setCameraAvailable(false)
cameraErrorHandler.handleCameraError(cameraState.error)
}
}
}
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() {
val scaleGesture = camera?.let { ScaleGestureDetector(activity, PinchToZoomOnScaleGestureListener(it.cameraInfo, it.cameraControl)) }
val gestureDetector = GestureDetector(activity, object : SimpleOnGestureListener() {
override fun onDown(event: MotionEvent): Boolean {
listener.onTouchPreview()
return super.onDown(event)
}
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()
val factory = DisplayOrientedMeteringPointFactory(display, it, width, height)
val xPos = event.x
val yPos = event.y
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(event.rawX, event.rawY)
true
} ?: false
}
})
previewView.setOnTouchListener { _, event ->
val handledGesture = gestureDetector.onTouchEvent(event)
val handledScaleGesture = scaleGesture?.onTouchEvent(event)
handledGesture || handledScaleGesture ?: false
}
}
override fun onStart(owner: LifecycleOwner) {
orientationEventListener.enable()
previewView.doOnLayout {
if (owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
startCamera()
}
}
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
if (config.saveMediaLocation) {
if (simpleLocationManager == null) {
simpleLocationManager = SimpleLocationManager(activity)
}
simpleLocationManager?.requestLocationUpdates()
}
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
simpleLocationManager?.dropLocationUpdates()
}
override fun onStop(owner: LifecycleOwner) {
orientationEventListener.disable()
}
override fun isInPhotoMode(): Boolean {
return isPhotoCapture
}
override fun showChangeResolution() {
val selectedResolution = if (isPhotoCapture) {
imageQualityManager.getUserSelectedResolution(cameraSelector).toResolutionOption()
} else {
videoQualityManager.getUserSelectedQuality(cameraSelector).toResolutionOption()
}
val resolutions = if (isPhotoCapture) {
imageQualityManager.getSupportedResolutions(cameraSelector).map { it.toResolutionOption() }
} else {
videoQualityManager.getSupportedQualities(cameraSelector).map { it.toResolutionOption() }
}
if (resolutions.size > 2) {
listener.showImageSizes(
selectedResolution = selectedResolution,
resolutions = resolutions,
isPhotoCapture = isPhotoCapture,
isFrontCamera = isFrontCameraInUse()
) { index, changed ->
mediaSizeStore.storeSize(isPhotoCapture, isFrontCameraInUse(), index)
if (changed) {
currentRecording?.stop()
startCamera()
}
}
} else {
toggleResolutions(resolutions)
}
}
private fun toggleResolutions(resolutions: List<ResolutionOption>) {
if (resolutions.size >= 2) {
val currentIndex = mediaSizeStore.getCurrentSizeIndex(isPhotoCapture, isFrontCameraInUse())
val nextIndex = if (currentIndex >= resolutions.lastIndex) {
0
} else {
currentIndex + 1
}
mediaSizeStore.storeSize(isPhotoCapture, isFrontCameraInUse(), nextIndex)
currentRecording?.stop()
startCamera()
}
}
override fun toggleFrontBackCamera() {
val newCameraSelector = if (isFrontCameraInUse()) {
CameraSelector.DEFAULT_BACK_CAMERA
} else {
CameraSelector.DEFAULT_FRONT_CAMERA
}
cameraSelector = newCameraSelector
config.lastUsedCameraLens = newCameraSelector.toLensFacing()
startCamera(switching = true)
}
override fun handleFlashlightClick() {
if (isPhotoCapture) {
listener.showFlashOptions(true)
} else {
toggleFlashlight()
}
}
private fun toggleFlashlight() {
val newFlashMode = if (isPhotoCapture) {
when (flashMode) {
FLASH_MODE_OFF -> FLASH_MODE_ON
FLASH_MODE_ON -> FLASH_MODE_AUTO
FLASH_MODE_AUTO -> FLASH_MODE_OFF
else -> throw IllegalArgumentException("Unknown mode: $flashMode")
}
} else {
when (flashMode) {
FLASH_MODE_OFF -> FLASH_MODE_ON
FLASH_MODE_ON -> FLASH_MODE_OFF
else -> throw IllegalArgumentException("Unknown mode: $flashMode")
}
}
setFlashlightState(newFlashMode.toAppFlashMode())
}
override fun setFlashlightState(state: Int) {
var flashState = state
if (isPhotoCapture) {
camera?.cameraControl?.enableTorch(flashState == FLASH_ALWAYS_ON)
} else {
camera?.cameraControl?.enableTorch(flashState == FLASH_ON || flashState == FLASH_ALWAYS_ON)
// reset to the FLASH_ON for video capture
if (flashState == FLASH_ALWAYS_ON) {
flashState = FLASH_ON
}
}
val newFlashMode = flashState.toCameraXFlashMode()
flashMode = newFlashMode
imageCapture?.flashMode = newFlashMode
config.flashlightState = flashState
listener.onChangeFlashMode(flashState)
}
override fun tryTakePicture() {
if (imageCapture == null) {
activity.toast(R.string.camera_open_error)
return
}
val imageCapture = imageCapture
val metadata = Metadata().apply {
isReversedHorizontal = isFrontCameraInUse() && config.flipPhotos
if (config.saveMediaLocation) {
location = simpleLocationManager?.getLocation()
}
}
val mediaOutput = mediaOutputHelper.getImageMediaOutput()
imageCapture!!.takePicture(mainExecutor, object : OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
listener.shutterAnimation()
playShutterSoundIfEnabled()
ensureBackgroundThread {
image.use {
if (mediaOutput is MediaOutput.BitmapOutput) {
val imageBytes = ImageUtil.jpegImageToJpegByteArray(image)
val bitmap = BitmapUtils.makeBitmap(imageBytes)
activity.runOnUiThread {
listener.onPhotoCaptureEnd()
if (bitmap != null) {
listener.onImageCaptured(bitmap)
} else {
cameraErrorHandler.handleImageCaptureError(ERROR_CAPTURE_FAILED)
}
}
} else {
ImageSaver.saveImage(
contentResolver = contentResolver,
image = image,
mediaOutput = mediaOutput,
metadata = metadata,
jpegQuality = config.photoQuality,
saveExifAttributes = config.savePhotoMetadata,
onImageSaved = { savedUri ->
activity.runOnUiThread {
listener.onPhotoCaptureEnd()
listener.onMediaSaved(savedUri)
}
},
onError = ::handleImageCaptureError
)
}
}
}
}
override fun onError(exception: ImageCaptureException) {
handleImageCaptureError(exception)
}
})
}
private fun handleImageCaptureError(exception: ImageCaptureException) {
listener.onPhotoCaptureEnd()
cameraErrorHandler.handleImageCaptureError(exception.imageCaptureError)
}
override fun initPhotoMode() {
debounceChangeCameraMode(photoModeRunnable)
}
override fun initVideoMode() {
debounceChangeCameraMode(videoModeRunnable)
}
private fun debounceChangeCameraMode(cameraModeRunnable: Runnable) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastCameraStartTime > CAMERA_MODE_SWITCH_WAIT_TIME) {
cameraModeRunnable.run()
} else {
cameraHandler.removeCallbacks(photoModeRunnable)
cameraHandler.removeCallbacks(videoModeRunnable)
cameraHandler.postDelayed(cameraModeRunnable, CAMERA_MODE_SWITCH_WAIT_TIME)
}
lastCameraStartTime = currentTime
}
override fun toggleRecording() {
if (currentRecording == null || recordingState is VideoRecordEvent.Finalize) {
if (config.isSoundEnabled) {
mediaSoundHelper.playStartVideoRecordingSound(onPlayComplete = {
startRecording()
})
listener.onVideoRecordingStarted()
} else {
startRecording()
}
} else {
currentRecording?.stop()
currentRecording = null
}
}
@SuppressLint("MissingPermission", "NewApi")
private fun startRecording() {
if (videoCapture == null) {
activity.toast(R.string.camera_open_error)
return
}
val videoCapture = videoCapture
val recording = when (val mediaOutput = mediaOutputHelper.getVideoMediaOutput()) {
is MediaOutput.FileDescriptorMediaOutput -> {
FileDescriptorOutputOptions.Builder(mediaOutput.fileDescriptor).apply {
if (config.saveMediaLocation) {
setLocation(simpleLocationManager?.getLocation())
}
}.build().let { videoCapture!!.output.prepareRecording(activity, it) }
}
is MediaOutput.FileMediaOutput -> {
FileOutputOptions.Builder(mediaOutput.file).apply {
if (config.saveMediaLocation) {
setLocation(simpleLocationManager?.getLocation())
}
}.build().let { videoCapture!!.output.prepareRecording(activity, it) }
}
is MediaOutput.MediaStoreOutput -> {
MediaStoreOutputOptions.Builder(contentResolver, mediaOutput.contentUri).apply {
setContentValues(mediaOutput.contentValues)
if (config.saveMediaLocation) {
setLocation(simpleLocationManager?.getLocation())
}
}.build().let { videoCapture!!.output.prepareRecording(activity, it) }
}
}
currentRecording = recording.withAudioEnabled()
.start(mainExecutor) { recordEvent ->
recordingState = recordEvent
when (recordEvent) {
is VideoRecordEvent.Start -> {
listener.onVideoRecordingStarted()
}
is VideoRecordEvent.Status -> {
listener.onVideoDurationChanged(recordEvent.recordingStats.recordedDurationNanos)
}
is VideoRecordEvent.Finalize -> {
playStopVideoRecordingSoundIfEnabled()
listener.onVideoRecordingStopped()
if (recordEvent.hasError()) {
cameraErrorHandler.handleVideoRecordingError(recordEvent.error)
} else {
listener.onMediaSaved(recordEvent.outputResults.outputUri)
}
}
}
}
}
private fun playShutterSoundIfEnabled() {
if (config.isSoundEnabled) {
mediaSoundHelper.playShutterSound()
}
}
private fun playStopVideoRecordingSoundIfEnabled() {
if (config.isSoundEnabled) {
mediaSoundHelper.playStopVideoRecordingSound()
}
}
}